# Accounts & sign-in
Source: https://docs.boat.dev/account
Sign-in methods, reinstalling or using a second machine, and which surface (CLI, API, SDKs, dashboard) does what.
## Sign-in methods
There are three ways to sign in, on the CLI and on the [dashboard](https://boat.dev/dashboard):
* GitHub
* Google
* A 6-digit code sent by email
No method is more correct than the others. Use the one you like. You can add the others later.
```bash theme={null}
boat login # asks: github, google, or email
boat login --google
boat login --email you@example.com
```
Each method that you use is a **connection** on your account. [Dashboard > Account](https://boat.dev/dashboard?tab=account) lists them.
* All connections open the same account, with the same sandboxes, settings and billing.
* You can add or remove connections at any time. One connection must remain.
* The **primary** connection sets the address that gets account notifications. To change that address, make a different connection primary.
A sign-in with a method that is not connected yet makes a **separate** account. This is true also when the email address is the same. To add a second method to your account, do these steps:
1. Sign in with your usual method.
2. Add the connection from the Account tab.
To move a connection between two existing accounts, you must merge the accounts. Only support can do a merge.
A sign-in with Google or email does not give Boat access to your code. To clone [repositories](/environments/repositories), you need a GitHub connection. You can add it when you first need it.
## Reinstalling, or a second machine
Your plan, environment, sandboxes and settings are on your account, not on your computer. To reinstall Boat, or to install it on a new computer, sign in on that computer:
```bash theme={null}
boat onboard # or: boat login
```
Sign in with the same method as the first time. `boat onboard` then finds your account:
* It skips each step that your account has already done.
* It does **not** open Stripe Checkout when your plan is active.
Use the same sign-in method as before. A method that was never connected to your account opens a **separate** account, with its own empty plan. This is the usual cause when a reinstall asks you to subscribe again.
## What the sandboxes are for
Onboarding asks what the sandboxes are for. There are two answers:
* Your own work.
* A platform of yours, used by third-party users. This is the default.
The second answer marks your environment [safe for third parties](/environments#safe-for-third-parties). Then none of your credentials go to those sandboxes.
You can change this at any time in [Dashboard > Environment](https://boat.dev/dashboard?tab=environment).
## Four ways to drive Boat
The four ways use the same platform. The code examples in these docs have **CLI**, **curl**, **TypeScript** and **Python** tabs. Select a tab one time, and the rest of the page uses it.
| | Use it for | Auth |
| - | - | - |
| **CLI** (`boat`) | your terminal, CI jobs, shell scripts, and from inside a sandbox | browser sign-in or `BOAT_API_KEY` |
| **REST API** | any language, webhooks, anything the SDKs do not cover yet | `BOAT_API_KEY` |
| **SDKs** ([Python](/sdks/python), [TypeScript](/sdks/typescript)) | typed calls from your own service | `BOAT_API_KEY` |
| **[Dashboard](https://boat.dev/dashboard)** | one-off work, and the things that must not be automatable | browser sign-in |
The four ways are not identical. The differences are intentional:
| Only in the dashboard | Only in the CLI | Only in the API and SDKs |
| - | - | - |
| [Webhook delivery history](/webhooks), [organization membership and invites](/organizations#members-and-invites), sign-in connections, per-sandbox metrics, [closing your account](/data-retention/close-account) | interactive SSH, SCP, port forwarding, `boat snapshot pull` | editing a webhook in place (`PATCH /webhooks/{webhookId}`), reading and writing files in a sandbox without SSH |
Some actions need a browser sign-in, on the CLI or on the dashboard. Boat refuses them to an API key on purpose:
* Create, rotate and revoke [API keys](/api-keys).
* Turn on [zero data retention](/data-retention).
An agent can also sign itself in and claim a human. See [Agent sign-in](/agent-auth).
# Agent sign-in
Source: https://docs.boat.dev/agent-auth
How an agent registers itself with Boat, claims a human, and starts the 7-day free trial.
An agent can join Boat without a human who pastes an API key. Boat implements [auth.md](https://workos.com/blog/agent-registration-with-auth-md), the WorkOS agentic registration protocol.
The contract is a machine-readable file at [ascii.dev/auth.md](https://boat.dev/auth.md). This page is a human-readable copy of it.
If you are an agent, fetch that file and follow it. Do not work from this page. Do not answer from search results about it.
## Start
Give the agent this instruction:
```
Follow https://boat.dev/auth.md and start a sandbox free trial for me.
```
The agent then does these steps:
1. It finds the endpoints from `/.well-known/oauth-protected-resource`.
2. It registers.
3. Only after that, it calls the Boat API.
## What the agent does
```mermaid theme={null}
sequenceDiagram
participant Agent
participant Boat
participant Human
Agent->>Boat: GET /auth.md and well-known metadata
Agent->>Boat: POST /agent/identity (anonymous or email)
alt anonymous
Boat-->>Agent: identity assertion (read-only)
else email
Boat-->>Agent: user_code + verification URL
end
Agent->>Human: Open this link and type this 6-digit code
Human->>Boat: Sign in, enter the code
Boat-->>Agent: access_token (Boat API key)
Agent->>Boat: POST /api/boat/billing/checkout
Agent->>Human: Open Stripe Checkout to start the 7-day trial
Human->>Boat: Card + trial
Agent->>Boat: POST /api/v1/sandboxes
```
There are two ways to register.
| Method | Token | What the agent can do |
| - | - | - |
| **Anonymous** | Immediately | Only `GET /me` and `GET /limits`. A sandbox create returns `claim_required` until a human claims the agent. |
| **Email (`service_auth`)** | After the person signs in and types the code | The agent must already know the user's email. |
Boat does not accept ID-JAG or provider-attested identity yet. The agent must not send `identity_assertion` registration bodies.
## Expect approval prompts
Almost every step sends a credential or an email address to a new host. Agent harnesses ask for approval before these actions. So the agent stops for approval more than once. This is correct behavior, not a failure.
You can retry each step by itself. When an approval stops the agent, it does these steps:
1. It says what it is about to send.
2. It waits for approval.
3. It continues from the step where it stopped.
## Trial
The trial uses the same Stripe Checkout that a human uses from the dashboard.
1. The agent calls `POST /api/boat/billing/checkout` with the claimed token.
2. The agent shows the URL that Boat returns.
3. The human enters a card and starts the trial.
These rules still apply:
* A card is necessary.
* Boat runs trial-abuse checks.
* There is one trial per identity.
* An anonymous token cannot start a trial.
## Prices
An agent must never give a price from memory or from a web search.
* `GET /api/v1/limits` returns the live plans, trial inclusions and caps. Read them there when necessary.
* To estimate usage cost, send the human to [boat.dev](https://boat.dev). It has the calculator.
* Search results and cached marketing pages are not a price source. Some of them describe different Ascii products.
## Endpoints
The endpoints are on `https://ascii.dev`. They are also under `/api/boat/...`, so they work behind the existing `/api` proxy.
| Path | Purpose |
| - | - |
| `/auth.md` | Agent-readable walkthrough |
| `/.well-known/oauth-protected-resource` | RFC 9728 discovery |
| `/.well-known/oauth-authorization-server` | RFC 8414 + `agent_auth` block |
| `POST /agent/identity` | Register |
| `POST /agent/identity/claim` | Start or refresh the claim ceremony |
| `GET /agent/identity/claim` | Human claim page |
| `POST /oauth2/token` | Exchange assertion or poll claim |
| `POST /oauth2/revoke` | Revoke one access token |
A 401 from the Boat API includes this header. An agent that only calls the API can use it to find the other endpoints.
```
WWW-Authenticate: Bearer resource_metadata="https://boat.dev/api/boat/.well-known/oauth-protected-resource"
```
# AI credentials
Source: https://docs.boat.dev/ai-credentials
Credential profiles, switching to another Claude or ChatGPT account when one runs out of credits, custom APIs, and the AI credentials proxy that keeps real keys out of your sandboxes.
Your sandboxes get the AI credentials of a **credential profile**: Claude and ChatGPT sign-ins, API keys, and custom APIs. With the **AI credentials proxy** on, a sandbox gets placeholder keys, and the real keys stay with Boat.
| | CLI | API | SDKs |
| - | - | - | - |
| List profiles | `boat ai profiles` | `GET /credential-profiles` | `listCredentialProfiles` |
| Create a profile | `boat ai profile new ` | `POST /credential-profiles` | `createCredentialProfile` |
| Rename, make default | `boat ai profile rename` / `default` | `PUT /credential-profiles/{profile}` | `updateCredentialProfile` |
| Delete a profile | `boat ai profile rm ` | `DELETE /credential-profiles/{profile}` | `deleteCredentialProfile` |
| Set, remove an API key | `boat ai profile set-key` / `rm-key` | `PUT`, `DELETE /credential-profiles/{profile}/keys/{credential}` | `setCredentialProfileKey`, `deleteCredentialProfileKey` |
| Switch a credential to another account | `boat ai accounts` / `switch` | `GET /credential-profiles/{profile}/accounts/{credential}`, `PUT …/{account}/active` | `listCredentialAccounts`, `switchCredentialAccount` |
| Add, forget an account | `boat ai add-account` / `forget-account` | `POST /credential-profiles/{profile}/accounts/{credential}`, `DELETE …/{account}` | `addCredentialAccount`, `deleteCredentialAccount` |
| Set, remove a custom API | `boat ai profile set-api` / `rm-api` | `PUT`, `DELETE /credential-profiles/{profile}/custom-apis/{name}` | `setCredentialProfileCustomApi`, `deleteCredentialProfileCustomApi` |
| Proxy on or off | `boat ai proxy [on\|off]` | `GET`, `PUT /ai-proxy` | `getAiProxy`, `updateAiProxy` |
| Usage | `boat ai usage [id]` | `GET /ai-usage`, `GET /sandboxes/{id}/ai-usage` | `getAiUsage`, `getSandboxAiUsage` |
| Block, unblock a sandbox | `boat ai block` / `unblock` | `PUT /sandboxes/{id}/ai-access` | `setSandboxAiAccess` |
The profile and proxy calls need an API key with `account.admin`. Account usage needs `account.read`. Sandbox usage needs `box.read` on that sandbox.
## Credential profiles
A credential profile is a named set of AI credentials. Boat encrypts it at rest. The API never returns a secret value: it lists which credentials a profile holds (`connected`) and the names of its custom APIs.
Each account has one **default** profile. A sandbox gets the default profile of its owner, unless its environment names another profile. The credentials you had before profiles existed are now the profile named `default`. On the dashboard **Agents & Proxying** page, pick a profile at the top. Both tabs then show and edit only that profile: its sign-ins and keys, its proxy switch, its custom APIs and its usage.
| Credential | Kind | Set it with |
| - | - | - |
| `anthropic-api-key`, `openai-api-key`, `openrouter-api-key`, `llmgateway-api-key`, `deepseek-api-key`, `moonshot-api-key`, `mistral-api-key` | API key | `set-key`, or the Agents & Proxying page |
| `claude-subscription`, `chatgpt-subscription`, `kimi-subscription`, `mistral-vibe-subscription` | Sign-in | The Agents & Proxying page |
| `claude-bedrock`, `codex-bedrock` | AWS Bedrock | The Agents & Proxying page |
You can remove any credential with `rm-key`. A sign-in sent to `set-key` returns `400 not_an_api_key`.
### Out of credits? Switch to another account
A credential can hold several accounts: two ChatGPT sign-ins, two Claude sign-ins, two OpenAI keys. One account is in use. When it runs out of credits, switch the credential to another account. Every sandbox that uses the profile gets the other account at once, running sandboxes included. Nothing restarts, and the other credentials do not change.
On the dashboard **Agents & Proxying** page, the account in use shows next to each credential. Click it to:
* **switch** to another account,
* **add another account**: sign in again, or paste another key. The current account stays, ready to switch back to,
* **forget** an account. Forgetting the one in use switches to another account first.
Sign-ins renew only while in use. An account set aside keeps its sign-in as it was, and renews when you switch back to it.
```bash CLI theme={null}
boat ai accounts chatgpt-subscription
# # ACCOUNT
# 1 you@work.dev in use
# 2 you@home.dev
boat ai switch chatgpt-subscription 2
# API keys: add a second key, then switch by number, or by its last characters
echo "$OPENAI_API_KEY_2" | boat ai add-account openai-api-key
boat ai switch openai-api-key …7f3a
boat ai forget-account openai-api-key 2
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/credential-profiles/default/accounts/chatgpt-subscription" \
-H "Authorization: Bearer $BOAT_API_KEY"
# use the id of the account to switch to, from the list
curl -sS -X PUT "$BOAT_API_BASE/credential-profiles/default/accounts/chatgpt-subscription/$ACCOUNT_ID/active" \
-H "Authorization: Bearer $BOAT_API_KEY"
curl -sS -X POST "$BOAT_API_BASE/credential-profiles/default/accounts/openai-api-key" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"value":"'"$OPENAI_API_KEY_2"'"}'
```
```ts TypeScript theme={null}
const { accounts } = await sandbox.listCredentialAccounts({ profile: "default", credential: "chatgpt-subscription" });
const other = accounts.find((a) => !a.active)!;
await sandbox.switchCredentialAccount({ profile: "default", credential: "chatgpt-subscription", account: other.id });
```
```python Python theme={null}
accounts = sandbox.list_credential_accounts("default", "chatgpt-subscription").accounts
other = next(a for a in accounts if not a.active)
sandbox.switch_credential_account("default", "chatgpt-subscription", other.id)
```
`--profile` picks another profile; the default is your default profile. The account list never shows a secret: a sign-in shows its email when the provider gives it, a key shows its last characters.
To give different sandboxes different accounts at the same time, use one profile per account and name the profile on an environment (see [Environments](#environments)).
### Set and remove API keys
`--copy-from` starts a new profile with a copy of the API keys and custom APIs of a profile you manage. Sign-ins are not copied: one sign-in in two profiles would sign itself out at the first renewal. Sign in again on the new profile. The CLI reads a key from stdin when you omit the value, so the key stays out of your shell history. A key is one token of up to 8192 printable characters, without spaces or line breaks; anything else returns `400`.
```bash CLI theme={null}
boat ai profile new ci --copy-from default
echo "$ANTHROPIC_API_KEY" | boat ai profile set-key ci anthropic-api-key
boat ai profile rm-key ci claude-subscription
boat ai profile rename ci ci-bots
boat ai profile rm ci-bots
```
```bash curl theme={null}
curl -sS -X POST "$BOAT_API_BASE/credential-profiles" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"ci","copyFrom":"default"}'
curl -sS -X PUT "$BOAT_API_BASE/credential-profiles/ci/keys/anthropic-api-key" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"value":"'"$ANTHROPIC_API_KEY"'"}'
curl -sS -X DELETE "$BOAT_API_BASE/credential-profiles/ci/keys/claude-subscription" \
-H "Authorization: Bearer $BOAT_API_KEY"
curl -sS -X DELETE "$BOAT_API_BASE/credential-profiles/ci" -H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
await sandbox.createCredentialProfile({ name: "ci", copyFrom: "default" });
await sandbox.setCredentialProfileKey({
profile: "ci",
credential: "anthropic-api-key",
value: process.env.ANTHROPIC_API_KEY!,
});
await sandbox.deleteCredentialProfileKey({ profile: "ci", credential: "claude-subscription" });
await sandbox.deleteCredentialProfile({ profile: "ci" });
```
```python Python theme={null}
import os
from boat_sdk.models.create_credential_profile_request import CreateCredentialProfileRequest
from boat_sdk.models.set_credential_key_request import SetCredentialKeyRequest
sandbox.create_credential_profile(CreateCredentialProfileRequest(name="ci", copy_from="default"))
sandbox.set_credential_profile_key("ci", "anthropic-api-key", SetCredentialKeyRequest(value=os.environ["ANTHROPIC_API_KEY"]))
sandbox.delete_credential_profile_key("ci", "claude-subscription")
sandbox.delete_credential_profile("ci")
```
Running sandboxes that use the profile get a changed key. You cannot delete a default profile (`409 is_default`) or a profile that an environment names (`409 in_use`).
### Organization profiles
An organization can own profiles too. Every active member can use them. Only organization owners create and change them, and only they see the account list. In a profile list, `owner.kind` is `org` and `canManage` tells you if you can change it.
```bash CLI theme={null}
boat --org acme ai profile new team-claude
echo "$ANTHROPIC_API_KEY" | boat ai profile set-key team-claude anthropic-api-key
boat ai profiles --org-only
```
```bash curl theme={null}
curl -sS -X POST "$BOAT_API_BASE/credential-profiles" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"team-claude","org":"acme"}'
```
```ts TypeScript theme={null}
await sandbox.createCredentialProfile({ name: "team-claude", org: "acme" });
```
```python Python theme={null}
sandbox.create_credential_profile(CreateCredentialProfileRequest(name="team-claude", org="acme"))
```
To give a team's sandboxes the organization profile, name it on an organization environment. See [Organizations & teams](/organizations/shared-sandboxes#shared-environments-and-ai-credentials).
## Custom APIs
A custom API is a secret for any HTTPS API, such as Stripe or your own service. You give it a name, a value and a base URL. The sandbox gets two variables:
| Variable | Value |
| - | - |
| `` | The secret. A placeholder when the proxy is on |
| `_BASE_URL` | The base URL. The proxy URL when the proxy is on |
The name is an environment variable name: `A-Z`, `0-9` and `_`, up to 64 characters. Provider names (`ANTHROPIC_*`, `OPENAI_*`, `AWS_*`, and so on) and names that end in `_BASE_URL` are reserved. The base URL must use HTTPS on the default port, with a public DNS name.
```bash CLI theme={null}
echo "$STRIPE_KEY" | boat ai profile set-api default STRIPE_KEY --base-url https://api.stripe.com/v1
boat ai profile rm-api default STRIPE_KEY
```
```bash curl theme={null}
curl -sS -X PUT "$BOAT_API_BASE/credential-profiles/default/custom-apis/STRIPE_KEY" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"value":"'"$STRIPE_KEY"'","baseUrl":"https://api.stripe.com/v1"}'
curl -sS -X DELETE "$BOAT_API_BASE/credential-profiles/default/custom-apis/STRIPE_KEY" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
await sandbox.setCredentialProfileCustomApi({
profile: "default",
name: "STRIPE_KEY",
value: process.env.STRIPE_KEY!,
baseUrl: "https://api.stripe.com/v1",
});
await sandbox.deleteCredentialProfileCustomApi({ profile: "default", name: "STRIPE_KEY" });
```
```python Python theme={null}
from boat_sdk.models.set_custom_api_request import SetCustomApiRequest
sandbox.set_credential_profile_custom_api("default", "STRIPE_KEY", SetCustomApiRequest(
value=os.environ["STRIPE_KEY"],
base_url="https://api.stripe.com/v1",
))
sandbox.delete_credential_profile_custom_api("default", "STRIPE_KEY")
```
Code in the sandbox must send its requests to `$_BASE_URL`. With the proxy on, a request sent straight to the API with the placeholder fails.
## The AI credentials proxy
Without the proxy, a sandbox holds your real keys and sign-ins. Anyone with a shell in it can copy them. With the proxy on, the real keys never enter the sandbox.
```
sandbox Boat provider
ANTHROPIC_API_KEY=
ANTHROPIC_BASE_URL=
agent ── request + placeholder ──▶ proxy: placeholder valid?
sandbox blocked?
swap in the real key ── request + real key ──▶ api.anthropic.com
◀──────── reply ───────────── count tokens ◀─────────── reply ──────────────
```
| | The sandbox holds | Boat holds |
| - | - | - |
| Proxy off | The real keys and sign-ins | The real keys and sign-ins |
| Proxy on | Placeholders and the proxy URL | The real keys and sign-ins, and renews the sign-ins |
* A placeholder is worthless outside Boat. It does not contain the real key.
* A copied placeholder stops working when the sandbox stops. Each start gives the sandbox new placeholders.
* The proxy sends each credential only to its own provider. An Anthropic key never goes to another host.
* The proxy puts a provider key only in the `Authorization` or `x-api-key` header. A placeholder in another header or in the URL stays a placeholder, so a provider that echoes it cannot return the real key.
* A custom API gets its secret in any request header and in the query string, never in the path. Pick a base URL you trust: a host that echoes requests shows the secret to the sandbox.
* Boat renews Claude and ChatGPT sign-ins behind the proxy. The sandbox never sees a renew token.
### What the proxy covers
| Proxied | Not proxied |
| - | - |
| Claude sign-in and Anthropic API key (Claude Code) | AWS Bedrock (`claude-bedrock`, `codex-bedrock`) |
| ChatGPT sign-in and OpenAI API key (Codex) | Kimi Code sign-in (`kimi-subscription`) |
| OpenRouter, LLM Gateway, DeepSeek, Moonshot API keys | |
| Mistral API key and Mistral Vibe sign-in | |
| Custom APIs | |
Bedrock signs each request with AWS keys, so there is nothing to swap. The Kimi Code CLI renews its sign-in itself. While the proxy is on, Boat withholds these credentials from sandboxes. Use a Moonshot API key in place of the Kimi sign-in.
### Turn it on
The switch is per account: one for you, one for each organization. Only an organization owner can change the organization switch. On the dashboard, open **Agents & Proxying**, then the **Proxying** tab. It holds the switch, the custom APIs and the AI usage, for the account in view: you, or the organization you selected.
A sandbox goes through the proxy when either switch is on:
* the switch of the account that owns its credential profile, or
* the switch of the organization that pays for the sandbox. An organization can make sure that no real key enters its sandboxes, whoever's profile they use.
Running sandboxes switch at their next start. Stop and resume a sandbox to switch it now.
```bash CLI theme={null}
boat ai proxy # state and price
boat ai proxy on
boat --org acme ai proxy on
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/ai-proxy" -H "Authorization: Bearer $BOAT_API_KEY"
curl -sS -X PUT "$BOAT_API_BASE/ai-proxy" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"enabled":true}'
curl -sS -X PUT "$BOAT_API_BASE/ai-proxy?org=acme" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"enabled":true}'
```
```ts TypeScript theme={null}
const proxy = await sandbox.getAiProxy();
console.log(proxy.enabled, proxy.pricing);
await sandbox.updateAiProxy({ enabled: true });
await sandbox.updateAiProxy({ org: "acme", enabled: true });
```
```python Python theme={null}
from boat_sdk.models.update_ai_proxy_request import UpdateAiProxyRequest
proxy = sandbox.get_ai_proxy()
print(proxy.enabled, proxy.pricing)
sandbox.update_ai_proxy(UpdateAiProxyRequest(enabled=True))
sandbox.update_ai_proxy(UpdateAiProxyRequest(enabled=True), org="acme")
```
The proxy applies to sandboxes that use a profile of an account with the switch on. A sandbox that uses an organization profile follows the organization switch.
## Safe for third parties, with AI credentials
An environment marked [safe for third parties](/environments#safe-for-third-parties) passes nothing of yours. Turn on `proxyAiCredentials` on that environment to still pass AI credentials, as placeholders. The sandbox user can run the agents, but cannot take your keys. You can block the sandbox at any time.
`proxyAiCredentials` needs the proxy on for the account that owns the profile. If it is off, the call returns `400 ai_proxy_off`.
```bash CLI theme={null}
boat ai proxy on
boat env set customers --safe-for-third-parties true --proxy-ai-credentials true --credential-profile ci
```
```bash curl theme={null}
curl -sS -X PUT "$BOAT_API_BASE/environments/$ENV_ID" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"safeForThirdParties":true,"proxyAiCredentials":true,"credentialProfileId":"ci"}'
```
```ts TypeScript theme={null}
await sandbox.updateEnvironment({
environmentId: envId,
safeForThirdParties: true,
proxyAiCredentials: true,
credentialProfileId: "ci",
});
```
```python Python theme={null}
from boat_sdk.models.update_sandbox_environment_request import UpdateSandboxEnvironmentRequest
sandbox.update_environment(env_id, UpdateSandboxEnvironmentRequest(
safe_for_third_parties=True,
proxy_ai_credentials=True,
credential_profile_id="ci",
))
```
## Environments
An environment can name the profile its sandboxes get, with `credentialProfileId` (an id or a name). `null`, or `--credential-profile default` in the CLI, means the default profile of the sandbox owner. Like every environment change, this makes a new version. Running sandboxes move at `boat env upgrade`. See [Environments](/environments/access#ai-credentials).
## Usage
For each proxied sandbox, Boat counts requests and errors, and the tokens per UTC day, credential and model. The token counts are the numbers the provider reports in its replies: input, output, cache read and cache write. A sandbox that holds real keys has no usage here, because its requests do not go through Boat.
```bash CLI theme={null}
boat ai usage # every sandbox of the account in scope
boat ai usage bx_23456789 --days 7 # one sandbox, per day and model
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/ai-usage?days=30" -H "Authorization: Bearer $BOAT_API_KEY"
curl -sS "$BOAT_API_BASE/sandboxes/bx_23456789/ai-usage?days=7" -H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
const account = await sandbox.getAiUsage({ days: 30 });
for (const s of account.sandboxes) console.log(s.sandboxId, s.totals.requests, s.dollars);
const one = await sandbox.getSandboxAiUsage({ sandboxId: "bx_23456789", days: 7 });
console.log(one.blocked, one.totals.inputTokens, one.totals.outputTokens);
```
```python Python theme={null}
account = sandbox.get_ai_usage(days=30)
for s in account.sandboxes:
print(s.sandbox_id, s.totals.requests, s.dollars)
one = sandbox.get_sandbox_ai_usage("bx_23456789", days=7)
print(one.blocked, one.totals.input_tokens, one.totals.output_tokens)
```
The account view covers the wallet in scope: your own, or an organization with `--org`, `org` or `X-Boat-Org`.
### Block a sandbox
Block a sandbox to stop its AI requests at once, running or stopped. The proxy refuses its placeholders with `403`. Unblock it to accept them again. Blocking has no effect on a sandbox that holds real keys (proxy off). The sandbox creator and the managers of its profile can block it.
```bash CLI theme={null}
boat ai block bx_23456789
boat ai unblock bx_23456789
```
```bash curl theme={null}
curl -sS -X PUT "$BOAT_API_BASE/sandboxes/bx_23456789/ai-access" \
-H "Authorization: Bearer $BOAT_API_KEY" -H "Content-Type: application/json" \
-d '{"blocked":true}'
```
```ts TypeScript theme={null}
await sandbox.setSandboxAiAccess({ sandboxId: "bx_23456789", blocked: true });
await sandbox.setSandboxAiAccess({ sandboxId: "bx_23456789", blocked: false });
```
```python Python theme={null}
from boat_sdk.models.set_sandbox_ai_access_request import SetSandboxAiAccessRequest
sandbox.set_sandbox_ai_access("bx_23456789", SetSandboxAiAccessRequest(blocked=True))
sandbox.set_sandbox_ai_access("bx_23456789", SetSandboxAiAccessRequest(blocked=False))
```
## Pricing
Profiles, custom APIs and usage tracking are free. The proxy costs money, paid in Boat time like named snapshots:
| Charge | Price | Paid by |
| - | - | - |
| Each UTC day the proxy is on | \$1 | The account with the switch on |
| Proxied requests | \$1 per 1,000 | The wallet each sandbox bills |
`dollars` in the usage responses is the request charge for the period. The provider bills your tokens as usual, on your own provider account.
## Related
* [Environments](/environments)
* [Organizations & teams](/organizations)
* [Integrated agents](/integrated-agents)
* [Pricing](/pricing)
* [CLI reference](/cli-reference#boat-ai)
# API Keys
Source: https://docs.boat.dev/api-keys
Scoped, expiring Boat API keys. Least privilege for production.
A Boat API key authenticates servers, CI and the in-sandbox CLI.
* Every **new** key expires.
* Every **new** key has an explicit scope. The scope is on the key, and the Boat API enforces it.
* The bearer header does not change.
Treat API keys as secrets. Store them as `BOAT_API_KEY`. Do not commit them, print them in logs or bake them into images.
Existing keys stay account-wide until you reissue them. Reissue every key that still has no expiry.
## Main task
1. [Create a key](/api-keys/create-and-list#create-a-key) with the smallest scope that does the job.
2. [Store it](/api-keys/store-rotate-revoke#store-a-key) as `BOAT_API_KEY` in your platform secret manager.
3. [Use it](/api-keys/create-and-list#use-the-key) from the CLI, the API or an SDK.
4. [Rotate or revoke it](/api-keys/store-rotate-revoke) when you need to.
## Production guidance
* Use one key per job.
* Use the shortest TTL that works for you.
* Use `read-only` or `ci` unless you truly need `admin`.
* Reissue legacy keys.
## Learn more
Actions, presets, the in-sandbox key and scope errors.
Create a key, use it, list your keys and see the usage of one key.
Where to keep a key, how to replace its secret and how to turn it off.
# Create and list keys
Source: https://docs.boat.dev/api-keys/create-and-list
Create a scoped Boat API key, use it, list your keys and see the usage of one key.
## Create a key
To create a key, you need one of these:
* A browser session: `boat login` with no key, or the dashboard.
* A token that is already admin-scoped.
The create endpoint is `POST /api/v1/api-keys/scoped`.
During a credential rollout, Boat can turn off key creation for a short time. `GET /api/v1/api-keys` reports this in `catalog.scopedCreationEnabled`. While creation is off, the create endpoint returns a typed 503.
```bash theme={null}
boat api-key create my-project --ttl 90d --preset ci --sandbox bx_123
boat api-key create readonly --ttl 30d --preset read-only
boat api-key create admin --ttl 7d --preset admin
boat api-key create custom --ttl 24h --actions sandbox.read,exec,file.read
```
| Flag | Rule |
| - | - |
| `--ttl` | The maximum is 365 days. |
| `--sandbox`, `--env` | You can give each one more than one time. |
| `--actions`, `--preset` | Use one or the other, not both. |
| No `--actions` and no `--preset` | The key is admin-scoped with a 90-day TTL. |
For the actions and presets, read [Scopes](/api-keys/scopes).
Boat shows the secret one time only. In scripts, capture it like this:
```bash theme={null}
BOAT_API_KEY="$(boat api-key create my-project --preset ci --ttl 90d --json | jq -r '.secret')"
```
You can also use the **API Keys** tab in the dashboard. It has expiry presets, action presets, a raw action picker, and sandbox and environment selectors.
## Use the key
```bash CLI theme={null}
boat login --key-stdin --json <<< "$BOAT_API_KEY"
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/me" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
import { BoatApi, Configuration } from "@boatdev/sdk";
const sandbox = new BoatApi(new Configuration({
basePath: "https://boat.dev/api/v1",
accessToken: process.env.BOAT_API_KEY!,
}));
const me = await sandbox.me();
console.log(me.user.login);
```
```python Python theme={null}
import os
from boat_sdk import ApiClient, Configuration
from boat_sdk.api.boat_api import BoatApi
config = Configuration(host="https://boat.dev/api/v1", access_token=os.environ["BOAT_API_KEY"])
with ApiClient(config) as client:
sandbox = BoatApi(client)
me = sandbox.me()
print(me.user.login)
```
## What a key cannot do
* Rotate and revoke need a browser session.
* Create needs a browser session or an admin-scoped token. A credential that can make more credentials defeats least privilege.
* To approve an agent claim, you need a browser session. API keys cannot approve agent claims. This includes admin keys and legacy keys.
Use `boat api-key create|rotate|revoke` after a browser sign-in, or use the [API Keys](https://boat.dev/dashboard?tab=api-keys) tab.
## List
Every surface can list keys. This is the only key operation that every surface can do. Boat never returns the secrets.
Each row shows:
* The id, the name, the prefix and the last four characters
* The scope and the expiry
* The last use
* The request total for the last 30 days
* How many sandboxes and Agents the key created that still exist
Boat labels expired keys and keys that expire soon.
Restricted API keys get an empty `apiKeys` list. Only a browser or CLI session, or an unrestricted account key, gets the account-wide list.
```bash theme={null}
boat api-key list
boat api-key list --all # include per-sandbox machine keys
```
```ts TypeScript theme={null}
const keys = await sandbox.apiKeys();
for (const key of keys.apiKeys) {
console.log(key.id, key.name, key.usage.requests, key.resources.total);
}
```
```python Python theme={null}
keys = sandbox.api_keys()
for key in keys.api_keys:
print(key.id, key.name, key.usage.requests, key.resources.total)
```
## See usage for one key
`boat api-key usage ` prints the 30-day request total and the count of live resources. These are the same numbers as in the list.
* Add `--verbose` to see the split between sandboxes and Agents, and the list of created resources.
* The matching API is `GET /api-keys/{id}/usage`.
* Usage still works after you revoke the key, if you still have the id.
Revoke stops the secret. It does not delete the sandboxes or Agents that the key created.
```bash theme={null}
boat api-key usage sak_123
boat api-key usage sak_123 --verbose
```
# Scopes
Source: https://docs.boat.dev/api-keys/scopes
What a scoped Boat API key can do: actions, presets, the in-sandbox key and scope errors.
## How Boat checks a request
Boat grants a request only when **both** of these are true:
1. The action is in the action set of the key.
2. The target sandbox is in the sandbox set of the key, or in one of its environments.
The default is deny. Boat refuses unknown routes for scoped keys.
## Snapshots and environments
| You want to | The key needs |
| - | - |
| Create a sandbox from a named snapshot | `sandbox.create` for the destination, and `snapshot.read` access to the source |
| Replace a named snapshot | Access to its existing source, and access to the sandbox that you save |
A destination environment does not give access to snapshots from other environments.
## Actions
| Action | What it unlocks |
| - | - |
| `sandbox.create` | Create a sandbox |
| `sandbox.read` | List and inspect sandboxes |
| `sandbox.update` | Rename, recover, and other sandbox writes |
| `sandbox.stop` | Stop |
| `sandbox.resume` | Resume |
| `sandbox.fork` | Fork |
| `sandbox.delete` | Delete |
| `sandbox.delete-own` | Delete only the sandboxes this key created |
| `agent.prompt` | Prompt the sandbox agent |
| `exec` | Run a command |
| `file.read` / `file.write` | Read or write files |
| `ssh` | SSH, scp, and port forward |
| `desktop` | Desktop stream |
| `host` | Host a port |
| `snapshot.read` / `snapshot.write` | Snapshots |
| `environment.read` / `environment.write` | Environments |
| `account.read` | `/me`, limits, list keys, organization discovery |
| `account.admin` | Billing, teams, webhooks, identities, account writes |
| `*` | Admin wildcard |
## Presets
| Preset | Use |
| - | - |
| `read-only` | Inspect sandboxes, files, snapshots, environments |
| `full-sandbox` | Operate a sandbox without create / resume / fork / delete / account writes |
| `ci` | Everything in `full-sandbox`, plus create, resume, fork, and delete the sandboxes this key created. No account admin |
| `admin` | `*` |
A key keeps the actions that it had when you created it. Resume, SSH and `sandbox.delete-own` joined the `ci` preset later. An older `ci` key does not get them. To get them, create a new `ci` key.
## In-sandbox key
Boat writes a credential into each sandbox. This credential is scoped to **that sandbox** only.
| It can | It cannot |
| - | - |
| Prompt, exec, read and write files | Create, resume, fork or delete sandboxes |
| SSH, desktop, host | Write environments |
| Snapshot its own sandbox | Administer the account |
If an attacker takes control of a sandbox, they get access to that one sandbox only.
## Errors
Scoped keys return typed 403 errors:
| `error` | Meaning |
| - | - |
| `api_key_expired` | The TTL elapsed |
| `api_key_action_forbidden` | The action is not on the key |
| `api_key_sandbox_forbidden` | The sandbox or the environment is not on the key |
The bearer header and the SDK configuration do not change.
# Store, rotate and revoke
Source: https://docs.boat.dev/api-keys/store-rotate-revoke
Where to store a Boat API key, how to replace its secret and how to turn it off.
## Store a key
| Platform | Store as |
| - | - |
| Railway | Variable named `BOAT_API_KEY` |
| GitHub Actions | Repository or environment secret named `BOAT_API_KEY` |
| Docker Compose | Environment variable or secret named `BOAT_API_KEY` |
| Kubernetes | Secret mounted or exposed as `BOAT_API_KEY` |
Do not put API keys in:
* Dockerfiles
* Images
* Source code
* Shell history
* Public CI logs
## Rotate a key
Rotate does these things:
* It revokes the old secret immediately.
* It keeps the id, the scope and the expiry date of the key.
* It replaces the secret and shows the new secret one time only.
Rotate does not extend the lifetime of the key. It does not change a legacy key into a scoped key.
You cannot rotate an expired scoped key. The API returns `409 api_key_expired`. Create a replacement key instead.
Rotate needs a browser session. Read [What a key cannot do](/api-keys/create-and-list#what-a-key-cannot-do).
### Rotate with downtime
Use **Rotate** only when you can update the deployed secret immediately.
1. Rotate the key. Run `boat api-key rotate `, or use the dashboard. To find the id, run `boat api-key list`.
2. Copy the new secret.
3. Update `BOAT_API_KEY` in your platform secret manager.
4. Redeploy or restart the workers that use the key.
### Replace without downtime
1. Create a new key.
2. Update the platform secret to the new key.
3. Redeploy or restart the workers.
4. When the new deployment is live, revoke the old key.
## Revoke a key
Run `boat api-key revoke `, or click **Revoke** in the dashboard. Revoke needs a browser session.
* Revoke turns off the key immediately.
* The next Boat API request with that secret fails with an auth error. This includes existing CLI configs and running processes.
* The sandboxes and Agents that the key created stay.
* `boat api-key usage ` still shows their totals.
# Create webhook
Source: https://docs.boat.dev/api/reference/account/create-webhook
openapi/boat-v1.yaml POST /webhooks
Create an account-wide sandbox lifecycle webhook endpoint.
# Delete webhook
Source: https://docs.boat.dev/api/reference/account/delete-webhook
openapi/boat-v1.yaml DELETE /webhooks/{webhookId}
Delete a sandbox lifecycle webhook endpoint and its queued deliveries.
# Get account data-retention policy
Source: https://docs.boat.dev/api/reference/account/get-account-data-retention-policy
openapi/boat-v1.yaml GET /account/data-retention
Read the account zero-data-retention setting.
# Get API key usage
Source: https://docs.boat.dev/api/reference/account/get-api-key-usage
openapi/boat-v1.yaml GET /api-keys/{apiKeyId}/usage
Get the 30-day request total and created sandboxes and Agents for one API key.
# Get Boat limits
Source: https://docs.boat.dev/api/reference/account/get-boat-limits
openapi/boat-v1.yaml GET /limits
Check remaining machine starts, compute time, credits, access readiness, and concurrent-sandbox capacity for the wallet a create would bill: the `org` / `X-Boat-Org` you pass (id or name), else the account's active wallet, else personal. The response carries `teamId` when an organization wallet was read.
# Get Boat secrets setup
Source: https://docs.boat.dev/api/reference/account/get-boat-secrets-setup
openapi/boat-v1.yaml GET /secrets
Returns the environment variables and secret files configured for sandboxes.
# Get current Boat user
Source: https://docs.boat.dev/api/reference/account/get-current-boat-user
openapi/boat-v1.yaml GET /me
Returns GitHub identity for the authenticated Boat account.
# Get deletion operation
Source: https://docs.boat.dev/api/reference/account/get-deletion-operation
openapi/boat-v1.yaml GET /deletion-operations/{operationId}
Poll an accepted sandbox or snapshot deletion operation.
# Get OpenTelemetry export
Source: https://docs.boat.dev/api/reference/account/get-opentelemetry-export
openapi/boat-v1.yaml GET /telemetry
Read the account's OTLP endpoint, header names, and last export result.
# Get webhook
Source: https://docs.boat.dev/api/reference/account/get-webhook
openapi/boat-v1.yaml GET /webhooks/{webhookId}
Get one sandbox lifecycle webhook endpoint without its signing secret.
# List API keys
Source: https://docs.boat.dev/api/reference/account/list-api-keys
openapi/boat-v1.yaml GET /api-keys
Lists API key metadata only. Raw key secrets are not returned after creation/rotation. Results include expiry and scope. A restricted API-key caller receives `apiKeys: []`; only an unrestricted account key or browser/CLI session receives the account inventory. Bearer header is unchanged; scope lives on the key.
# List GitHub repositories available to Boat
Source: https://docs.boat.dev/api/reference/account/list-github-repositories-available-to-sandbox
openapi/boat-v1.yaml GET /repos
Returns GitHub repositories grouped by installation plus the current selected repositories for new Sandboxes.
# List organizations
Source: https://docs.boat.dev/api/reference/account/list-organizations
openapi/boat-v1.yaml GET /orgs
The wallets this account can bill: your personal account first, then every organization you belong to, each with its id and name. Either spelling works wherever an org is passed (`org`, `X-Boat-Org`, `teamId`). `active` marks the wallet new sandboxes bill when a request names none; change it with `PATCH /orgs/active`.
# List webhooks
Source: https://docs.boat.dev/api/reference/account/list-webhooks
openapi/boat-v1.yaml GET /webhooks
List account-wide sandbox lifecycle webhook endpoints.
# Remove OpenTelemetry export
Source: https://docs.boat.dev/api/reference/account/remove-opentelemetry-export
openapi/boat-v1.yaml DELETE /telemetry
Stop the export and remove the account's OTLP endpoint.
# Rotate webhook signing secret
Source: https://docs.boat.dev/api/reference/account/rotate-webhook-signing-secret
openapi/boat-v1.yaml POST /webhooks/{webhookId}/rotate
Replace a webhook signing secret and return the new secret once.
# Select repository for sandboxes
Source: https://docs.boat.dev/api/reference/account/select-repository-for-sandboxes
openapi/boat-v1.yaml POST /repos
Idempotently selects one repository for the sandbox environment. Use `databaseId` from `GET /repos` as `repositoryId`; selecting an already-selected repository updates its `baseBranch`.
# Set the active organization
Source: https://docs.boat.dev/api/reference/account/set-active-organization
openapi/boat-v1.yaml PATCH /orgs/active
Set the wallet new sandboxes bill when a request names none, for this account on every client: the API, the CLI (`boat org switch` sets the same value) and the dashboard. Pass an organization id or name you belong to, or `personal` (or `null`) for your own account. `GET /limits` follows it. Sandboxes already running keep the wallet they were created with; leaving or deleting the organization resets the account to personal.
# Set OpenTelemetry export
Source: https://docs.boat.dev/api/reference/account/set-opentelemetry-export
openapi/boat-v1.yaml PUT /telemetry
Set or replace the account's OTLP endpoint and send a test export.
# Update account data-retention policy
Source: https://docs.boat.dev/api/reference/account/update-account-data-retention-policy
openapi/boat-v1.yaml PATCH /account/data-retention
Enable or disable account zero data retention using a sandbox session.
# Update Boat secrets setup
Source: https://docs.boat.dev/api/reference/account/update-boat-secrets-setup
openapi/boat-v1.yaml POST /secrets
Save a new version of your account's sandbox environment variables and secret files. Omitted fields stay unchanged. Send an empty value to clear one. Running sandboxes keep their current version until you upgrade them.
# Update webhook
Source: https://docs.boat.dev/api/reference/account/update-webhook
openapi/boat-v1.yaml PATCH /webhooks/{webhookId}
Update a sandbox lifecycle webhook endpoint.
# Attach to a detached command
Source: https://docs.boat.dev/api/reference/agent/attach-command
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/commands/{processId}/stream
Stream the output of a command started with detached: true as it writes it, until it ends. Reconnect with the offsets every 10 minutes to follow it with no time limit.
# Configure sandbox SSH key
Source: https://docs.boat.dev/api/reference/agent/configure-sandbox-ssh-key
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/sshkey
Adds an OpenSSH public key to the running sandbox so the caller can SSH as user `user`. The response carries `machineIp`, which is the host to connect to on port 22.
This is also how you reach your own machine from inside a sandbox without the CLI: authorize the key, then open a reverse tunnel with stock OpenSSH, and a port on your machine answers at `127.0.0.1:` inside the sandbox for as long as the ssh process runs.
```bash
ssh -o ExitOnForwardFailure=yes -i ~/.ssh/id_ed25519 -N -R 7777:127.0.0.1:7777 user@
```
`boat forward --reverse --local 7777` is the same tunnel with the key handling and redial done for you.
# Get a download URL for a sandbox file
Source: https://docs.boat.dev/api/reference/agent/create-download-url
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/files/download-url
A short-lived signed URL the sandbox serves itself. GET it to download a file of any size.
# Get an upload URL for a sandbox file
Source: https://docs.boat.dev/api/reference/agent/create-upload-url
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/files/upload-url
A short-lived signed URL the sandbox serves itself. PUT a file of any size to it.
# Download Sandbox artifact
Source: https://docs.boat.dev/api/reference/agent/download-sandbox-artifact
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/artifacts
Download a file from a sandbox by its path, returned as a binary artifact.
# Execute Sandbox command
Source: https://docs.boat.dev/api/reference/agent/execute-sandbox-command
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/commands
Runs the command synchronously by default (timeout configurable via `timeoutSeconds`, 600s cap). With `detached: true` the command starts in the background and a process id is returned immediately; poll `/sandboxes/{sandboxId}/commands/{processId}` for status and logs. With `stream: true` the output arrives as the command writes it, as newline-delimited JSON (`application/x-ndjson`): `{"type":"started"}`, then `{"type":"stdout"|"stderr","data":"..."}` per chunk, and last `{"type":"exit",...}` (the synchronous result without stdout/stderr) or `{"type":"error","error":"...","message":"..."}`. Returns 400 invalid_timeout when `timeoutSeconds` is not an integer in 1-600. A command sent while the sandbox is still starting (right after a resume) waits up to 60s for it to be ready instead of failing; 409 boat_starting (retryable) is returned only if it is still not ready by then. A sandbox that is not starting (`error`, `stopped`) returns 409 sandbox_not_ready with `retryable: false` and its `state` at once: resume or recover it first. Command execution is never retried automatically: a 502 boat_direct_failed means the command may already be running on the sandbox. Every command and its output are kept 90 days in the command history (`/sandboxes/{sandboxId}/command-history`), readable while the sandbox is stopped.
# Get a command from the history
Source: https://docs.boat.dev/api/reference/agent/get-command
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/command-history/{commandId}
Get one command's record: its text, mode, status, exit code and output size.
# Get command status
Source: https://docs.boat.dev/api/reference/agent/get-command-status
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/commands/{processId}
Get the status and recent log output of a detached command running in a sandbox. Use tailBytes to limit how much log is returned.
# Get desktop streaming URL
Source: https://docs.boat.dev/api/reference/agent/get-desktop-streaming-url
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/desktop
Create or fetch a secret-bearing desktop/noVNC URL for live computer-use visibility. Use `?vnc=1` for noVNC; a `provisioning: true` response means poll again.
# Get prompt run status
Source: https://docs.boat.dev/api/reference/agent/get-prompt-run-status
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/prompts/{promptId}
Returns first-class status for a queued/running/finished prompt so clients do not infer completion from sandbox state and events.
# Interrupt running work
Source: https://docs.boat.dev/api/reference/agent/interrupt-running-agent-work
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/interrupt
Interrupt running agent work. By default this stops **every** conversation on the sandbox. Pass `conversation` to stop just one, leaving the others running. See [Integrated agents](/integrated-agents).
# List the command history
Source: https://docs.boat.dev/api/reference/agent/list-command-history
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/command-history
List the commands run in a sandbox in the last 90 days, newest first. Works while the sandbox is stopped.
# List boat conversations
Source: https://docs.boat.dev/api/reference/agent/list-sandbox-conversations
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/conversations
Every conversation on the sandbox, most recently prompted first, with what you need to pick one: how many prompts it holds, whether a turn is running in it right now, the last harness and model it ran on, a preview of its last prompt, and `current`, the one a `POST /prompt` without `new` or `conversationId` would continue. Pass an `id` back as `conversationId` on `POST /prompt` to resume that thread, or as `conversation` on `POST /steer`, `POST /interrupt` and `GET /events` to scope those. See [Integrated agents](/integrated-agents).
# List boat events
Source: https://docs.boat.dev/api/reference/agent/list-sandbox-events
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/events
Return Sandbox work, lifecycle, and progress events so product UIs can show what the sandbox is doing. A Sandbox runs many conversations in parallel; this streams **all** of them by default and every event carries a `conversationId`. Filter to one or more with `conversation`. Event payloads are extensible. Clients can long-poll this endpoint with `sort=asc` and a cursor to stream responses. Response events expose agent text, streaming partials via `data.is_streaming`, and tool calls/results via `data.tools`.
# List sandbox processes
Source: https://docs.boat.dev/api/reference/agent/list-sandbox-processes
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/processes
List the processes in a running sandbox, or the last stored sample of a stopped one.
# Prompt Sandbox
Source: https://docs.boat.dev/api/reference/agent/prompt-sandbox-agent
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/prompt
Queue a natural-language work item for an agent harness (Codex, Claude Code, pi, OpenCode, or Prime Agent) inside the sandbox. Observe progress with `GET /sandboxes/{sandboxId}/events`.
A Sandbox runs many conversations in parallel. Set `new: true` to start a fresh one, `conversationId` to continue a specific one, or omit both to continue the most-recently-active conversation; the response returns the `conversationId`. See [Integrated agents](/integrated-agents).
# Read a command's output
Source: https://docs.boat.dev/api/reference/agent/read-command-logs
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/command-history/{commandId}/logs
Read the stored stdout or stderr of a command from a byte offset. Works while the sandbox is stopped.
# Read Sandbox file
Source: https://docs.boat.dev/api/reference/agent/read-sandbox-file
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/files
Read a file from a sandbox by its path, as UTF-8 text by default or base64 for binary content.
# Steer a running turn
Source: https://docs.boat.dev/api/reference/agent/steer-a-running-turn
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/steer
Send a new message to a turn that is already running, without losing what the agent is doing. This is what typing into a coding agent while it works does: the instruction is taken into account and the work continues.
Where the harness has a native mid-turn primitive (Claude Code, Codex, pi, Prime Agent) nothing is interrupted and the response has `native: true`. Where it does not (OpenCode), Boat interrupts that turn and immediately continues the SAME conversation with the instruction, keeping the session and its memory, and the response has `native: false`.
Refused with **409 `no_running_turn`** when the conversation has nothing in flight; queue work with `POST /prompt` instead. `error.details.reason` says why: `turn_ended` (with `error.details.lastTurn.status` and `.error` of the newest turn), `turn_not_started` (the turn is starting; retry in a moment), `no_turn`, `not_running` (the sandbox is not running) or `delivery_failed` (the sandbox did not accept the steer; check the turn before sending more work). The steer shows up in `GET /events` as a `steer` event. See [Integrated agents](/integrated-agents).
# Stop a detached command
Source: https://docs.boat.dev/api/reference/agent/stop-command
openapi/boat-v1.yaml DELETE /sandboxes/{sandboxId}/commands/{processId}
Stop a command started with detached: true, and every process it started. The response holds its final history record.
# Write Sandbox file
Source: https://docs.boat.dev/api/reference/agent/write-sandbox-file
openapi/boat-v1.yaml PUT /sandboxes/{sandboxId}/files
Write or overwrite a file in a sandbox at a given path, as UTF-8 text or base64-encoded binary content.
# Add another API key to a credential
Source: https://docs.boat.dev/api/reference/ai-credentials/add-credential-account
openapi/boat-v1.yaml POST /credential-profiles/{profile}/accounts/{credential}
Keep a second key ready to switch to.
# Block or unblock sandbox AI access
Source: https://docs.boat.dev/api/reference/ai-credentials/block-or-unblock-sandbox-ai-access
openapi/boat-v1.yaml PUT /sandboxes/{sandboxId}/ai-access
Refuse or accept a sandbox's AI placeholders again.
# Create a credential profile
Source: https://docs.boat.dev/api/reference/ai-credentials/create-credential-profile
openapi/boat-v1.yaml POST /credential-profiles
Create a personal or organization AI credential profile, empty or as a copy.
# Delete a credential profile
Source: https://docs.boat.dev/api/reference/ai-credentials/delete-credential-profile
openapi/boat-v1.yaml DELETE /credential-profiles/{profile}
Delete a profile that is not the default and that no environment names.
# Forget an account of a credential
Source: https://docs.boat.dev/api/reference/ai-credentials/forget-credential-account
openapi/boat-v1.yaml DELETE /credential-profiles/{profile}/accounts/{credential}/{account}
Delete an account; the one in use is replaced by another first.
# Get AI credentials proxy
Source: https://docs.boat.dev/api/reference/ai-credentials/get-ai-credentials-proxy
openapi/boat-v1.yaml GET /ai-proxy
Read the AI credentials proxy switch and its price.
# Get AI usage
Source: https://docs.boat.dev/api/reference/ai-credentials/get-ai-usage
openapi/boat-v1.yaml GET /ai-usage
Proxied AI usage of every sandbox of the wallet in scope.
# Get sandbox AI usage
Source: https://docs.boat.dev/api/reference/ai-credentials/get-sandbox-ai-usage
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/ai-usage
Proxied AI usage of one sandbox, per day, credential and model.
# List the accounts of a credential
Source: https://docs.boat.dev/api/reference/ai-credentials/list-credential-accounts
openapi/boat-v1.yaml GET /credential-profiles/{profile}/accounts/{credential}
Every account of one credential, the one in use first.
# List credential profiles
Source: https://docs.boat.dev/api/reference/ai-credentials/list-credential-profiles
openapi/boat-v1.yaml GET /credential-profiles
List the AI credential profiles you can use, yours and your organizations'.
# Remove a credential profile credential
Source: https://docs.boat.dev/api/reference/ai-credentials/remove-credential-profile-credential
openapi/boat-v1.yaml DELETE /credential-profiles/{profile}/keys/{credential}
Remove one API key or sign-in from a profile.
# Remove a custom API
Source: https://docs.boat.dev/api/reference/ai-credentials/remove-custom-api
openapi/boat-v1.yaml DELETE /credential-profiles/{profile}/custom-apis/{name}
Remove one custom API from a profile.
# Set a credential profile API key
Source: https://docs.boat.dev/api/reference/ai-credentials/set-credential-profile-api-key
openapi/boat-v1.yaml PUT /credential-profiles/{profile}/keys/{credential}
Set one API key in a profile.
# Set a custom API
Source: https://docs.boat.dev/api/reference/ai-credentials/set-custom-api
openapi/boat-v1.yaml PUT /credential-profiles/{profile}/custom-apis/{name}
Add or replace a custom API: a secret and its HTTPS base URL.
# Switch a credential to another account
Source: https://docs.boat.dev/api/reference/ai-credentials/switch-credential-account
openapi/boat-v1.yaml PUT /credential-profiles/{profile}/accounts/{credential}/{account}/active
Out of credits on one account: every sandbox on the profile moves to another one at once.
# Turn the AI credentials proxy on or off
Source: https://docs.boat.dev/api/reference/ai-credentials/turn-the-ai-credentials-proxy-on-or-off
openapi/boat-v1.yaml PUT /ai-proxy
Give sandboxes placeholder keys instead of real keys, or stop.
# Update a credential profile
Source: https://docs.boat.dev/api/reference/ai-credentials/update-credential-profile
openapi/boat-v1.yaml PUT /credential-profiles/{profile}
Rename a profile, or make it its owner's default.
# Add environment repository
Source: https://docs.boat.dev/api/reference/environments/add-environment-repo
openapi/boat-v1.yaml POST /environments/{environmentId}/repos
Adds a repository (or updates its base branch) by database id from the repository list. Mints a new immutable version.
# Create a sandbox environment
Source: https://docs.boat.dev/api/reference/environments/create-sandbox-environment
openapi/boat-v1.yaml POST /environments
Create a new named environment. It starts on version 1 with all fine-grained flags on and `safeForThirdParties` off.
# Remove environment repository
Source: https://docs.boat.dev/api/reference/environments/delete-environment-repo
openapi/boat-v1.yaml DELETE /environments/{environmentId}/repos/{repositoryId}
Removes one repository from the environment's selection. Mints a new immutable version.
# Remove environment secret file
Source: https://docs.boat.dev/api/reference/environments/delete-environment-secret-file
openapi/boat-v1.yaml DELETE /environments/{environmentId}/secret-files
Removes the secret file at `path`. Mints a new immutable version.
# Remove environment variable
Source: https://docs.boat.dev/api/reference/environments/delete-environment-var
openapi/boat-v1.yaml DELETE /environments/{environmentId}/vars/{key}
Removes a single variable. Mints a new immutable version.
# Delete a sandbox environment
Source: https://docs.boat.dev/api/reference/environments/delete-sandbox-environment
openapi/boat-v1.yaml DELETE /environments/{environmentId}
Soft-deletes an environment. The default environment cannot be deleted, and at least one environment is always kept.
# List Sandbox environments
Source: https://docs.boat.dev/api/reference/environments/list-sandbox-environments
openapi/boat-v1.yaml GET /environments
Returns every non-deleted sandbox environment with its latest-version flags, contents, and per-version sandbox usage. A default environment named `base` is created on first access if none exists.
# Write environment secret file
Source: https://docs.boat.dev/api/reference/environments/set-environment-secret-file
openapi/boat-v1.yaml PUT /environments/{environmentId}/secret-files
Adds or replaces a single secret file by path. Mints a new immutable version.
# Set environment variable
Source: https://docs.boat.dev/api/reference/environments/set-environment-var
openapi/boat-v1.yaml PUT /environments/{environmentId}/vars/{key}
Sets or replaces a single variable without reading or rewriting the rest. Mints a new immutable version.
# Update a sandbox environment
Source: https://docs.boat.dev/api/reference/environments/update-sandbox-environment
openapi/boat-v1.yaml PUT /environments/{environmentId}
Rename, set as default, and/or edit flags and contents. Any flag or content change mints a new immutable version; existing sandboxes stay on their pinned version until you call upgrade.
# Upgrade sandboxes to an environment's latest version
Source: https://docs.boat.dev/api/reference/environments/upgrade-sandbox-environment
openapi/boat-v1.yaml POST /environments/{environmentId}/upgrade
Repoints the caller's active sandboxes from an older version of this environment to its latest version, scrubbing any owner secrets the new version drops and hot-pushing the new config.
# Create sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/create-sandbox
openapi/boat-v1.yaml POST /sandboxes
Provision a new cloud computer. Store the returned `sandbox.id` with your product job/session record. Send an `Idempotency-Key` header to make this call safe to retry after a lost response without creating a second billable sandbox.
# Fork sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/fork-sandbox
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/fork
Provision a new sandbox from an existing one. Send an `Idempotency-Key` header to make this call safe to retry after a lost response without creating a second billable fork.
# Get sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/get-sandbox
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}
Poll this endpoint after create, stop, resume, or fork operations.
# Get sandbox usage
Source: https://docs.boat.dev/api/reference/sandboxes/get-sandbox-usage
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/usage
Machine time one sandbox consumed and its cost, over its life or a billing window, for per-sandbox billing of your own users.
# Get a stop by id
Source: https://docs.boat.dev/api/reference/sandboxes/get-stop
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/stops/{stopId}
Returns one stop of this sandbox by the id that `POST /sandboxes/{sandboxId}/stop` returned. Old stops stay readable after a resume or a later stop. Returns 404 when the id is unknown or belongs to another sandbox.
# List sandboxes
Source: https://docs.boat.dev/api/reference/sandboxes/list-sandboxes
openapi/boat-v1.yaml GET /sandboxes
Your sandboxes. In an organization's scope (the active wallet, `org`, or `X-Boat-Org`) the list also holds every sandbox that organization pays for, whoever created it; `createdBy` and `access` say whose it is and what you may do with it.
# Permanently delete sandbox data
Source: https://docs.boat.dev/api/reference/sandboxes/permanently-delete-sandbox-data
openapi/boat-v1.yaml DELETE /sandboxes/{sandboxId}
Confirm and accept irreversible background deletion of a sandbox.
# Recover sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/recover-sandbox
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/recover
Power-cycle the machine of a sandbox that stopped responding, without touching its disk.
# Resume sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/resume-sandbox
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/resume
Resume a stopped sandbox from its snapshot, optionally with a different environment or time-to-live.
# Share sandbox with the organization
Source: https://docs.boat.dev/api/reference/sandboxes/share-sandbox
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/share
Let every member of the organization that pays for this sandbox use it.
Your personal logins (GitHub token, Claude and Codex logins, environment secrets,
the sandbox's own CLI key) are never pushed to it again and are wiped off its disk
at its next start, so a teammate never acts as you. Files you wrote stay. One-way:
the sandbox stays like a `noEnv` sandbox from then on. A running sandbox stays
`view` for the organization until you stop and resume it (`restartRequired`).
Only the person who created the sandbox can share it, and only a sandbox billed to
an organization. Sharing an already shared sandbox is a no-op.
# Stop and archive sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/stop-and-archive-sandbox
openapi/boat-v1.yaml POST /sandboxes/{sandboxId}/stop
Stop active work and archive/snapshot the sandbox for later resume or fork.
A stop always saves the sandbox's disk first. If that save is failing, the stop is
refused and the sandbox keeps running so your work is not discarded, we retry
automatically and email you. You are not billed for time spent in that state.
The stop is an operation: the response and the sandbox carry it as `stop`, with an
`id` and a `status`. Poll `GET /sandboxes/{sandboxId}` until `stop.status` is
`completed`. A failed attempt shows `failing` with its `error` and is retried
until the sandbox archives; commands and prompts you send meanwhile do not cancel
it. Only a resume ends it early (`superseded`). Calling stop again while one is
pending returns the same `id`.
Set `force: true` to stop anyway, keeping the last successful snapshot and
losing everything written since then. This is irreversible; only reach for it
after a stop has already failed. A force on a stop that is already pending
escalates that stop.
# Update sandbox
Source: https://docs.boat.dev/api/reference/sandboxes/update-sandbox
openapi/boat-v1.yaml PATCH /sandboxes/{sandboxId}
Update a sandbox's name, time-to-live, or the subdomain it is hosted on.
# Share a named snapshot
Source: https://docs.boat.dev/api/reference/snapshots/create-snapshot-share
openapi/boat-v1.yaml POST /named-snapshots/{name}/shares
Make a secret code that lets any Boat account create a sandbox from this named snapshot (`boat snapshot share ` in the CLI). The response shows the code once; Boat stores only a hash. Re-saving the name changes what the code deploys. Removing the name removes its codes. Everything on the disk is shared, so do not keep secrets in files on it. Needs `snapshot.write`.
# Delete a named snapshot
Source: https://docs.boat.dev/api/reference/snapshots/delete-named-snapshot
openapi/boat-v1.yaml DELETE /named-snapshots/{name}
Remove a named snapshot and release its storage (`boat snapshot rm ` in the CLI). Sandboxes already deployed from it are unaffected.
# Get latest boat snapshot
Source: https://docs.boat.dev/api/reference/snapshots/get-latest-sandbox-snapshot
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/snapshots/latest
Return the most recent completed snapshot for this sandbox, or `null` if it has none.
# Get a named snapshot
Source: https://docs.boat.dev/api/reference/snapshots/get-named-snapshot
openapi/boat-v1.yaml GET /named-snapshots/{name}
Get a named snapshot and its status. Poll this after saving one until the status settles at ready or failed.
# Get snapshot download
Source: https://docs.boat.dev/api/reference/snapshots/get-snapshot-download
openapi/boat-v1.yaml GET /snapshots/{snapshotId}/download
Return time-limited signed URLs for every chunk needed to reconstruct the sandbox filesystem at this snapshot (the full chain, base through this generation), plus the inventory. Download the chunks and reassemble them as described by `reconstruct`. The `boat snapshot pull` CLI command does this for you.
# Download a file or folder from a snapshot
Source: https://docs.boat.dev/api/reference/snapshots/get-snapshot-file
openapi/boat-v1.yaml GET /snapshots/{snapshotId}/files
Stream a single file's bytes, or a folder subtree as a tar archive, directly out of a snapshot, the sandbox can be stopped or archived; the machine is never contacted. Use `GET /snapshots/{snapshotId}/tree` to list paths, then pass one here. Paths are relative to the snapshot root (same space as `tree` entries; docker volumes under `__dockervol__/`). An empty or `/` path downloads the whole snapshot as a tar. Folder responses set `X-Snapshot-File-Count`, `X-Snapshot-Total-Size-Bytes` and `X-Snapshot-Skipped-Base-Image-Files` headers; both response shapes set `X-Snapshot-Entry-Kind` (`file` or `dir`). File responses accept a single `Range: bytes=` header and answer 206 with `Content-Range`, so large files can be downloaded in parts and resumed.
# Get snapshot file tree
Source: https://docs.boat.dev/api/reference/snapshots/get-snapshot-tree
openapi/boat-v1.yaml GET /snapshots/{snapshotId}/tree
List the files and folders captured in a snapshot, with sizes. Returns a flat list of entries you can render as a tree.
# List named snapshots
Source: https://docs.boat.dev/api/reference/snapshots/list-named-snapshots
openapi/boat-v1.yaml GET /named-snapshots
List your named snapshots, newest first.
# List boat snapshots
Source: https://docs.boat.dev/api/reference/snapshots/list-sandbox-snapshots
openapi/boat-v1.yaml GET /sandboxes/{sandboxId}/snapshots
List visible completed snapshots for one sandbox with uncapped cursor pagination.
# List share codes of a named snapshot
Source: https://docs.boat.dev/api/reference/snapshots/list-snapshot-shares
openapi/boat-v1.yaml GET /named-snapshots/{name}/shares
List the share codes of a named snapshot, active and revoked (`boat snapshot shares ` in the CLI). The full codes are not returned. Needs `snapshot.read`.
# List snapshots
Source: https://docs.boat.dev/api/reference/snapshots/list-snapshots
openapi/boat-v1.yaml GET /snapshots
List visible completed snapshots across all of your sandboxes using immutable ownership attribution. Pinned, deletion-scheduled, and privacy-fenced sandbox snapshots are omitted. Follow `nextCursor` through the complete history; there is no hidden 500-snapshot cap.
# Permanently delete snapshot data
Source: https://docs.boat.dev/api/reference/snapshots/permanently-delete-snapshot-data
openapi/boat-v1.yaml DELETE /snapshots/{snapshotId}
Confirm and accept irreversible background deletion of a snapshot.
# Revoke a share code
Source: https://docs.boat.dev/api/reference/snapshots/revoke-snapshot-share
openapi/boat-v1.yaml DELETE /named-snapshots/{name}/shares/{shareId}
Revoke a share code (`boat snapshot unshare ` in the CLI). The code stops creating new sandboxes. Sandboxes already created from it keep their own copy. Needs `snapshot.write`.
# Save a named snapshot
Source: https://docs.boat.dev/api/reference/snapshots/save-named-snapshot
openapi/boat-v1.yaml POST /named-snapshots
Freeze a sandbox's current state under a name (`boat snapshot ` in the CLI). Answers `202` immediately with the snapshot in `saving`; a running sandbox takes a fresh capture first, which can run minutes. Poll `GET /named-snapshots/{name}` until the status settles at `ready` or `failed`. Reusing one of your existing names replaces that snapshot's artifact once the new save is ready.
# Boat Public API v1
Source: https://docs.boat.dev/api/v1
Create, manage, prompt, observe, and expose sandboxes from your own products.
Use the Boat Public API from services, CI jobs, hosted workers, and product automation code. For clients with types, start with the [Boat SDKs](/sdks/overview), the [Python SDK](/sdks/python), or the [TypeScript/JavaScript SDK](/sdks/typescript).
## Recommended flow
1. Create a Boat API key with `boat api-key create` or in the dashboard.
2. Create a sandbox with `POST /sandboxes`. If the user continues earlier work, resume or fork a sandbox instead.
3. Poll `GET /sandboxes/{sandboxId}` until the sandbox is `ready` or `idle`.
4. Queue work with `POST /sandboxes/{sandboxId}/prompt`.
5. Read `GET /sandboxes/{sandboxId}/events` while the work runs.
6. If the user or your support team must see the computer live, use `POST /sandboxes/{sandboxId}/desktop`.
7. Stop (archive), resume, fork, or delete the sandbox as your retention policy requires.
The API does all that the Boat CLI does.
* Dedicated endpoints cover lifecycle, prompts, events, files, desktop, and snapshots.
* For all other actions, use [`POST /sandboxes/{sandboxId}/commands`](/api/reference/agent/execute-sandbox-command). Examples are in-sandbox tools like `host`, package installs, and your own scripts.
* For high-frequency or streaming control, run your own daemon in the sandbox. See [the daemon pattern](/platform-guide/agents#bring-your-own-harness-the-daemon-pattern).
## Base URL
```text theme={null}
https://boat.dev/api/v1
```
## Authentication
Send a Boat API key as a bearer token on each API request:
```bash theme={null}
curl -sS "https://boat.dev/api/v1/sandboxes" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
To create, rotate, and revoke service keys, use the `boat api-key` CLI command or the Boat dashboard. For the key lifecycle, see [API Keys](/api-keys).
Keep Boat API keys and returned desktop URLs secret. Desktop and VNC URLs can contain access tokens. Redact them before you log or store them.
`PATCH /account/data-retention` is the exception. It needs an interactive sandbox session and refuses API keys. A per-sandbox key works only on its sandbox. For reads across the account, use an account service key.
## Response envelope
Each v1 JSON response has an explicit field that tells success or failure:
```json theme={null}
{
"ok": true,
"type": "sandbox.list",
"sandboxes": []
}
```
| Field | Description |
| - | - |
| `ok` | `true` for success, `false` for failure. |
| `type` | Stable response or event discriminator, such as `sandbox.list`, `sandbox.created`, `sandbox.stopping`, or `prompt.queued`. |
## Error model
The HTTP status is the primary signal. Error bodies also use a structured JSON envelope:
```json theme={null}
{
"ok": false,
"type": "sandbox.error",
"status": 409,
"code": "provider_not_configured",
"message": "Prompting is locked until Codex is configured on the Agents page.",
"error": {
"code": "provider_not_configured",
"message": "Prompting is locked until Codex is configured on the Agents page.",
"status": 409,
"details": {
"provider": "codex",
"setupUrl": "https://boat.dev/dashboard?tab=agents"
}
},
"requestId": "req_..."
}
```
| Status | Typical codes | What to do |
| - | - | - |
| `400` | `invalid_json`, `prompt_required`, `provider_required`, `invalid_name`, `no_changes`, `invalid_path`, `invalid_timeout`, `invalid_env`, `unknown_environment`, `trial_auto_stop_required`, `fast_mode_not_supported` | Fix the request. |
| `400` | `machine_not_running` (desktop, browser, port hosting, SSH key) | The sandbox has no running machine. Resume it, then retry. |
| `401` | `unauthorized` | Provide a valid bearer token. |
| `402` | `billing_required`, `team_member_cap_reached` | Send the user to the returned billing URL, or ask the organization owner to raise the member's usage cap. |
| `403` | `trial_machine_class_not_allowed`, `org_suspended` | The requested machine size needs a payment method, or the organization wallet is suspended. |
| `404` | `not_found` | The sandbox or key does not exist anymore. Refresh your local state. |
| `409` | `account_not_ready`, `provider_not_configured`, `sandbox_not_promptable`, `resume_failed`, `fork_failed`, `sandbox_resuming`, `sandbox_stopping`, `sandbox_securing`, `snapshots_disabled`, `idempotency_in_progress`, `idempotency_key_reused` | Resolve the prerequisite or resource-state conflict. |
| `409` | `machine_not_running`, `sandbox_starting`, `sandbox_not_ready` (commands, files, processes) | See [Commands, files and processes](#commands-files-and-processes). |
| `429` | `rate_limited`, `daily_limit_reached`, `limit_reached`, `member_limit_reached` | Back off and show the limit message. |
| `500` | `http_500` and other `*_failed` codes | Retry idempotent calls with jitter. Include `requestId` when you contact support. |
| `502` | `sandbox_direct_failed` | See [Commands, files and processes](#commands-files-and-processes). |
| `503` | `browser_not_ready`, `ssh_host_key_unavailable` | Retry after a few seconds. |
### Commands, files and processes
These calls go to the machine of the sandbox:
* `POST /sandboxes/{sandboxId}/commands`
* `GET` and `PUT /sandboxes/{sandboxId}/files`
* The process calls
They can also fail because of the sandbox state.
| Status | Code | Retryable | Meaning |
| - | - | - | - |
| `409` | `machine_not_running` | No | The sandbox has no machine. It is stopped, or it never started. Call `POST /sandboxes/{sandboxId}/resume`, wait until `GET` reports `ready`, `idle` or `running`, then retry. |
| `409` | `sandbox_starting` | Yes | The sandbox is still starting. Retry every few seconds. |
| `409` | `sandbox_not_ready` | No | The sandbox is in a state that cannot run commands, such as `error` or `archived`. Resume it first. |
| `502` | `sandbox_direct_failed` | See `error.details.retryable` | The machine did not complete the call. When `retryable` is `true`, you can safely send the call again. A file read or write gives the same result again. A command with message `sandbox_restoring` did not run. A command with a different message possibly ran. Check its effect before you send it again. When `retryable` is `false` (a command that failed or timed out), the command can still be running. Do not run it again without a check. |
| `502` | `sandbox_direct_failed` with message `sandbox_restoring` | Yes | The sandbox is mounting its files in the first seconds of a resume or fork. Retry after 1-3 seconds. |
The response body has the flag in `error.details.retryable`:
```json theme={null}
{
"ok": false,
"type": "sandbox.error",
"status": 502,
"code": "sandbox_direct_failed",
"message": "sandbox_restoring",
"error": {
"code": "sandbox_direct_failed",
"status": 502,
"details": { "retryable": true, "message": "sandbox_restoring" }
}
}
```
## Idempotent sandbox creation
`POST /sandboxes` and `POST /sandboxes/{sandboxId}/fork` each make a new sandbox that bills when it succeeds. A response can get lost, for example a timeout or a `5xx` after Boat accepted the sandbox. Then you do not know if you can retry.
To make create and fork safe to retry, send an `Idempotency-Key` header. Use your own value that is unique in your account, for example a UUID.
1. The first request creates the sandbox and links it to the key.
2. Each retry with the **same account, key, and request body** returns that same sandbox. It does not create a second sandbox that bills.
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff" \
-H "Content-Type: application/json" \
-d '{"from":"web-stack","noEnv":true,"ttlSeconds":1800}'
```
| Situation | Result |
| - | - |
| Retry after a lost `202` or a `5xx` | Same key returns the original sandbox (no second sandbox). |
| Retry while the first sandbox is still being created | `409` `idempotency_in_progress`. Retry shortly with the same key. |
| Same key, different request body | `409` `idempotency_key_reused`. |
| Create that failed before the sandbox existed | Key auto-releases within \~2 minutes so a retry can create the sandbox. |
Boat keeps keys for 24 hours. A key belongs to your account, so two accounts can use the same key value without a conflict. Without the header, create and fork are not idempotent. That is the default. The header is optional on the create and fork calls of each SDK (`Idempotency-Key` / `idempotencyKey`). See [Create sandbox](/api/reference/sandboxes/create-sandbox) and [Fork sandbox](/api/reference/sandboxes/fork-sandbox).
## Sandbox lifecycle
A sandbox goes through these states:
| State | Meaning | Client behavior |
| - | - | - |
| `provisioning`, `provisioned`, `cloning` | The sandbox is being created or prepared. | Poll `GET /sandboxes/{sandboxId}`. |
| `ready`, `idle` | The sandbox can accept prompts and interactive access. | Prompt, open desktop, SSH, or inspect events. |
| `running` | The sandbox is working. | Read events. Interrupt only when the caller wants to stop the current work. |
| `archiving` | Stop/snapshot is in progress. | Poll until `archived` or another terminal state. |
| `archived` | The machine is stopped. A snapshot can be available. | Resume or fork. |
| `error` | Provisioning or runtime failed. | Show the error and let the user stop, delete, resume, or fork when allowed. |
`idle` and `running` show only work queued through `POST /sandboxes/{sandboxId}/prompt`. Processes that you run yourself, over SSH or the command endpoint, do not change the state. A sandbox can show `idle` while your own agent works inside it.
## Providers, models, and reasoning
`POST /sandboxes/{sandboxId}/prompt` runs work through an agent harness (provider):
| Provider | Aliases | Notes |
| - | - | - |
| `codex` | | OpenAI Codex / ChatGPT models |
| `claude-code` | `claude` | Claude Code models |
| `pi` | | Multi-model: Anthropic, OpenAI, and OpenRouter/llmgateway-routed models |
| `opencode` | | Multi-model |
| `prime-agent` | `prime` | Multi-model |
| `kimi` | `kimi-code` | Kimi Code CLI: Kimi models (subscription or Moonshot API key) |
| `mistral` | | Mistral Vibe: Mistral models (Mistral Vibe subscription or Mistral API key) |
Without `provider`, Boat uses the harness that the user selected on the Agents dashboard.
**Models and reasoning efforts depend on the harness and the model, and they change over time.** Get them from the live catalog. Do not hardcode lists.
* `GET /provider-models` returns each model for each provider, and the `reasoningEffort` levels that it accepts: `none`, `low`, `medium`, `high`, `xhigh`, `max`. Some models accept none.
* The `boat prompt --help` CLI command prints the same catalog.
* `pi`, `opencode`, and `prime-agent` also give models routed through OpenRouter and llmgateway (ids like `openrouter:anthropic/claude-sonnet-4.5`).
* Without `model`, Boat uses the default that the user saved on the dashboard for that harness.
To run a prompt in fast mode, set `fast: true`. The model stays the same and gives output faster. The provider bills it at its higher fast rate.
* Without `fast`, Boat uses the choice saved on the Agents dashboard.
* `fast: false` forces standard speed.
* Only models with `"fastMode": true` in `GET /provider-models` accept fast mode. Other models return `400 fast_mode_not_supported`.
Before you send a prompt, set the credentials on the provider setup page of the dashboard. If credentials are missing, the API returns `provider_not_configured` with a setup URL.
A sandbox runs **many conversations in parallel**. `POST /prompt` takes `new` or `conversationId` and returns the `conversationId`. `GET /events` and `POST /interrupt` take a `conversation` filter. See [Integrated agents](/integrated-agents).
## Request examples
These examples are raw HTTP. Each one has a CLI, TypeScript and Python equivalent on its feature page: [Snapshots](/snapshots), [Environments](/environments), [Webhooks](/webhooks), [Data retention](/data-retention), [Desktop](/desktop-streaming), [Hosting](/hosting). The [CLI reference](/cli-reference) maps commands to endpoints.
Set shared variables:
```bash theme={null}
export BOAT_API_BASE="https://boat.dev/api/v1"
export BOAT_API_KEY="sandbox_your_secret_here"
```
Create a one-hour sandbox and store its id:
```bash theme={null}
BOAT_ID=$(curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttlSeconds":3600}' | jq -r '.sandbox.id')
```
Create a sandbox without automatic archival:
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttlSeconds":null}'
```
Poll readiness:
```bash theme={null}
curl -sS "$BOAT_API_BASE/sandboxes/$BOAT_ID" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
Prompt a sandbox to work in a repo:
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/prompt" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "codex",
"model": "gpt-5.4",
"reasoningEffort": "medium",
"prompt": "Work on the selected repo: run tests, fix failures, commit the result, and report the preview URL if you start a server."
}'
```
Read the work and lifecycle events of the sandbox:
```bash theme={null}
curl -sS "$BOAT_API_BASE/sandboxes/$BOAT_ID/events" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
Get a desktop streaming URL for live inspection:
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/desktop?vnc=1" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
To return a VNC URL that does not require an access token, send:
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/desktop?vnc=1" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"publicAccess":true}'
```
Stop (archive) a sandbox when the workflow is complete:
```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/stop" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
An archive keeps the snapshots for a resume. A permanent delete needs the exact target id in a confirmation header. It returns `202`, and you cannot cancel it:
```bash theme={null}
curl -sS -X DELETE "$BOAT_API_BASE/sandboxes/$BOAT_ID" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "X-Ascii-Confirm-Delete: $BOAT_ID"
```
Poll `GET /deletion-operations/{operationId}` until `status` is `completed`. See [Data retention and deletion](/data-retention).
## Endpoint reference
| Endpoint | Reference |
| - | - |
| `GET /me` | [Get current Boat user](/api/reference/account/get-current-boat-user) |
| `GET /limits` | [Get Boat limits](/api/reference/account/get-boat-limits) |
| `GET /account/data-retention` | [Get account data-retention policy](/api/reference/account/get-account-data-retention-policy) |
| `PATCH /account/data-retention` | [Update account data-retention policy](/api/reference/account/update-account-data-retention-policy) |
| `GET /deletion-operations/{operationId}` | [Get deletion operation](/api/reference/account/get-deletion-operation) |
| `GET /repos` | [List GitHub repositories available to Boat](/api/reference/account/list-github-repositories-available-to-sandbox) |
| `POST /repos` | [Select repository for sandboxes](/api/reference/account/select-repository-for-sandboxes) |
| `GET /api-keys` | [List API keys](/api/reference/account/list-api-keys) |
| `GET /api-keys/{apiKeyId}/usage` | [Get API key usage](/api/reference/account/get-api-key-usage) |
| `GET /secrets` | [Get Boat secrets setup](/api/reference/account/get-boat-secrets-setup) |
| `POST /secrets` | [Update Boat secrets setup](/api/reference/account/update-boat-secrets-setup) |
| `GET /sandboxes` | [List sandboxes](/api/reference/sandboxes/list-sandboxes) |
| `POST /sandboxes` | [Create sandbox](/api/reference/sandboxes/create-sandbox) |
| `GET /sandboxes/{sandboxId}` | [Get sandbox](/api/reference/sandboxes/get-sandbox) |
| `GET /sandboxes/{sandboxId}/usage` | [Get boat usage](/api/reference/sandboxes/get-sandbox-usage) |
| `PATCH /sandboxes/{sandboxId}` | [Update sandbox](/api/reference/sandboxes/update-sandbox) |
| `DELETE /sandboxes/{sandboxId}` | [Permanently delete sandbox data](/api/reference/sandboxes/permanently-delete-sandbox-data) |
| `POST /sandboxes/{sandboxId}/stop` | [Stop and archive sandbox](/api/reference/sandboxes/stop-and-archive-sandbox) |
| `GET /sandboxes/{sandboxId}/stops/{stopId}` | [Get a stop by id](/api/reference/sandboxes/get-stop) |
| `POST /sandboxes/{sandboxId}/resume` | [Resume sandbox](/api/reference/sandboxes/resume-sandbox) |
| `POST /sandboxes/{sandboxId}/recover` | [Recover sandbox](/api/reference/sandboxes/recover-sandbox) |
| `POST /sandboxes/{sandboxId}/fork` | [Fork sandbox](/api/reference/sandboxes/fork-sandbox) |
| `POST /sandboxes/{sandboxId}/prompt` | [Prompt Sandbox](/api/reference/agent/prompt-sandbox-agent) |
| `GET /sandboxes/{sandboxId}/events` | [List boat events](/api/reference/agent/list-sandbox-events) |
| `GET /sandboxes/{sandboxId}/prompts/{promptId}` | [Get prompt run status](/api/reference/agent/get-prompt-run-status) |
| `GET /sandboxes/{sandboxId}/files` | [Read a file from a sandbox](/api/reference/agent/read-sandbox-file) |
| `PUT /sandboxes/{sandboxId}/files` | [Write a file in a sandbox](/api/reference/agent/write-sandbox-file) |
| `POST /sandboxes/{sandboxId}/files/download-url` | [Get a download URL for a sandbox file](/api/reference/agent/create-download-url) |
| `POST /sandboxes/{sandboxId}/files/upload-url` | [Get an upload URL for a sandbox file](/api/reference/agent/create-upload-url) |
| `POST /sandboxes/{sandboxId}/commands` | [Execute a command in a sandbox](/api/reference/agent/execute-sandbox-command) |
| `GET /sandboxes/{sandboxId}/artifacts` | [Download a sandbox artifact](/api/reference/agent/download-sandbox-artifact) |
| `POST /sandboxes/{sandboxId}/interrupt` | [Interrupt running work](/api/reference/agent/interrupt-running-agent-work) |
| `POST /sandboxes/{sandboxId}/desktop` | [Get desktop streaming URL](/api/reference/agent/get-desktop-streaming-url) |
| `POST /sandboxes/{sandboxId}/sshkey` | [Configure sandbox SSH key](/api/reference/agent/configure-sandbox-ssh-key) |
| `GET /snapshots` | [List snapshots](/api/reference/snapshots/list-snapshots) |
| `GET /sandboxes/{sandboxId}/snapshots` | [List boat snapshots](/api/reference/snapshots/list-sandbox-snapshots) |
| `GET /sandboxes/{sandboxId}/snapshots/latest` | [Get latest boat snapshot](/api/reference/snapshots/get-latest-sandbox-snapshot) |
| `GET /snapshots/{snapshotId}/tree` | [Get snapshot file tree](/api/reference/snapshots/get-snapshot-tree) |
| `GET /snapshots/{snapshotId}/files` | [Download a file or folder from a snapshot](/api/reference/snapshots/get-snapshot-file) |
| `GET /snapshots/{snapshotId}/download` | [Get snapshot download](/api/reference/snapshots/get-snapshot-download) |
| `DELETE /snapshots/{snapshotId}` | [Permanently delete snapshot data](/api/reference/snapshots/permanently-delete-snapshot-data) |
# Billing details
Source: https://docs.boat.dev/billing
Plan time versus credit packs, per-sandbox usage, and what happens when you run out.
The [Pricing](/pricing) page gives prices, sizes and plans. This page tells how billing works under them.
## Plan time vs credit packs
Your balance has two pools. They work differently, and the difference shows only at renewal.
| Pool | When you get it | At the end of the month |
| - | - | - |
| **Plan time** | Fresh at the start of every billing month | It expires. Boat does **not** carry it forward and does not convert it into credits. |
| **Credit packs** (\$20 buys 2,000,000 seconds) | When you buy a pack | It never expires. It carries over to the next month. |
**Boat always spends plan time first.** It uses credits only after the plan time of the month is gone. Credits that Boat uses are gone for good. The next renewal gives back your plan time, but never refills your credits.
Example: a \$20/month subscriber also bought one \$20 pack. They used 2.5M seconds in one month.
| | Plan time | Credits |
| - | - | - |
| Used this month | All 2M seconds | 500,000 seconds |
| Left at the start of next month | 2M fresh seconds | 1.5M seconds |
Check your balance at any time:
```bash CLI theme={null}
boat limits
boat limits --json
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/limits" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
const limits = await sandbox.limits();
console.log(limits.canStart, limits.starts?.day?.remaining, limits.creditBalanceHours, limits.packBalanceDollars);
```
```python Python theme={null}
limits = sandbox.limits()
day = getattr(limits.starts, "day", None)
print(limits.can_start, getattr(day, "remaining", None), limits.credit_balance_hours, limits.pack_balance_dollars)
```
For every field, read [Get Boat limits](/api/reference/account/get-boat-limits).
## Per-sandbox usage
Every sandbox meters its own machine time. A platform can give each of its users or agents a sandbox. It can then bill them from the same numbers that drain your balance. Read one sandbox at any time, while it runs or after it stops:
```bash CLI theme={null}
boat usage bx_23456789
boat usage bx_23456789 --since 2026-09-01 --until 2026-10-01
boat usage bx_23456789 --json
```
```bash curl theme={null}
curl -sS "$BOAT_API_BASE/sandboxes/bx_23456789/usage?since=2026-09-01T00:00:00Z&until=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $BOAT_API_KEY"
```
```ts TypeScript theme={null}
const usage = await sandbox.usage({ sandboxId: "bx_23456789", since: "2026-09-01T00:00:00Z", until: "2026-10-01T00:00:00Z" });
console.log(usage.seconds, usage.dollars, usage.running);
```
```python Python theme={null}
usage = sandbox.usage("bx_23456789", since="2026-09-01T00:00:00Z", until="2026-10-01T00:00:00Z")
print(usage.seconds, usage.dollars, usage.running)
```
| Field | Means |
| - | - |
| `seconds` | Billable machine time, with the multiplier of the sandbox type already applied. A `large` sandbox that ran ten minutes reads 1200. |
| `dollars` | `seconds` at list price (`seconds / secondsPerDollar`). |
| `running` | `true` while the sandbox runs and the number grows. `false` after it stops. Also `false` while a refused stop keeps the meter paused. |
* Boat never counts stopped time.
* Boat does not count time after a [refused stop](/snapshots/stop#when-a-stop-is-refused).
* Omit `since` and `until` to get the full life of the sandbox.
* If a billing period boundary falls inside a running stretch, Boat splits that stretch pro rata.
For every field, read [Get boat usage](/api/reference/sandboxes/get-sandbox-usage).
## AI credentials proxy
The [AI credentials proxy](/ai-credentials#the-ai-credentials-proxy) is paid in Boat time from the same balance, like named snapshots above the first 10:
| Charge | Price | Paid by |
| - | - | - |
| Each UTC day the proxy is on | \$1 | The account with the switch on |
| Proxied requests | \$1 per 1,000 | The wallet each sandbox bills |
`boat ai usage` and `GET /ai-usage` show the requests per sandbox, and `dollars` is their charge.
## Organizations
A team shares one plan and one balance through an organization. Everything else on this page applies to an organization in the same way. [Organizations & teams](/organizations) covers members, the active wallet, shared sandboxes, per-member caps and how to move a personal plan in.
## Running out
When your balance reaches zero:
1. You get a **24 hour grace period**. This is true with or without auto-pay.
2. Your sandboxes keep running. Boat sends you an email alert. You can top up before anything stops.
3. If you do not pay in that window, Boat stops your running sandboxes with a snapshot.
4. Nothing is lost. Resume the sandboxes after you top up.
To never reach that stop, turn on **auto-pay** in the billing dashboard. When you run out, auto-pay charges your saved card in small automatic amounts. It uses any prepaid balance on your account first.
If a monthly renewal payment fails and you still have credits, your sandboxes keep running until the credits run out.
Sandboxes keep running through the grace period. So you can use a little more machine time than you had.
* `creditBalanceSeconds` stops at zero. It reads `0` while you owe this time.
* Boat carries the shortfall and settles it on your next payment.
* You pay for exactly the seconds that you used.
## Related
* [Pricing](/pricing)
* [Organizations & teams](/organizations)
* [Build a Platform on Boat](/platform-guide)
* [FAQ](/faq)
* [Long-Running Tasks](/long-running-tasks)
# CLI Reference
Source: https://docs.boat.dev/cli-reference
Complete reference for all Boat CLI commands and global flags.
## Installation & updates
```bash theme={null}
# Install (run once)
curl -fsSL https://boat.dev/install | sh
# Check for and apply updates
boat self-update
```
The CLI checks for updates each time it runs. To skip this check, pass `--no-update`.
***
## Global flags
These flags work with all commands:
| Flag | Description |
| - | - |
| `--api-url ` | Use a different API endpoint (env: `BOAT_API_URL`) |
| `--org ` | Run this command against an organization wallet (env: `BOAT_ORG`). The scope that you set with `boat org switch` does not change. |
| `--json` | Print JSON for machines instead of text for people |
| `--no-update` | Skip the automatic update check |
When you pipe or redirect the output, `--json` turns on automatically. Scripts get JSONL without the flag.
A command that takes a sandbox ID also accepts two aliases:
| Alias | Sandbox |
| - | - |
| `current` | The last sandbox that you created in this shell |
| `self` | The sandbox that the CLI runs inside, when you use the CLI in a sandbox |
***
## In scripts and CI
In an application, use the [Python or TypeScript SDK](/sdks/overview) or the [HTTP API](/api/v1) with a `BOAT_API_KEY`. You do not install a binary, and the responses have types. Use the CLI in shell scripts and CI jobs.
Sign in without the key in the process arguments. Then read JSON from stdout:
```bash theme={null}
boat login --key-stdin --json <<< "$BOAT_API_KEY"
sandbox_id="$(boat new --json | jq -r 'select(.event == "ready") | .id')"
boat exec "$sandbox_id" "npm test"
boat stop "$sandbox_id" --json
```
Most commands print one JSON object. These commands print JSON Lines, one object per line:
* `boat new`
* `boat prompt`
* `boat events`, with or without `--follow`
* `boat conversations`
Parse JSON Lines one line at a time.
When a command fails in `--json` mode, it prints a last line and exits with a non-zero code:
```text theme={null}
{"event":"error","error":"...","code":"...","status":...}
```
The line has `code` and `status` when the Boat API refused the request. Errors in the arguments go to stderr, and they are possibly not JSON.
To use the CLI in a Docker image, do these steps:
1. Download the static binary for the architecture of the image.
2. Sign in when the container starts.
```dockerfile theme={null}
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl openssh-client \
&& rm -rf /var/lib/apt/lists/*
RUN set -eu; \
case "$(uname -m)" in \
x86_64|amd64) arch=x64 ;; \
arm64|aarch64) arch=arm64 ;; \
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://boat.dev/api/boat/cli/download?platform=linux-${arch}&channel=prod" -o /usr/local/bin/boat; \
chmod +x /usr/local/bin/boat
```
```sh theme={null}
#!/bin/sh
# entrypoint
set -eu
printf '%s' "$BOAT_API_KEY" | boat login --key-stdin --json
exec node server.js
```
***
## Shell completion
`boat completions ` prints a completion script for bash, zsh, fish, or PowerShell. The script completes these values:
* Subcommands.
* The IDs of your live sandboxes, and `current` and `self`.
The script keeps the results for 15 seconds, so the next tabs are instant.
```bash theme={null}
# bash: add to ~/.bashrc
eval "$(boat completions bash)"
# zsh: add to ~/.zshrc, after compinit
eval "$(boat completions zsh)"
# fish
boat completions fish > ~/.config/fish/completions/sandbox.fish
# PowerShell: add to $PROFILE
boat completions powershell | Out-String | Invoke-Expression
```
***
## Authentication
### `boat onboard`
Set up this computer. The command does these steps:
1. It signs you in through the browser.
2. It saves your token.
3. It asks only for what your account still needs: the third-party safety answer and the plan checkout.
```bash theme={null}
boat onboard
boat onboard --google
boat onboard --email you@example.com
```
It asks which sign-in method to use: GitHub, Google, or a 6-digit code that it sends to your email. `--google` and `--email` give that answer in advance. See [Quickstart](/account#sign-in-methods).
You can run it again safely. On a new install or a second computer, it finds the account that you already have. It skips each step that this account has done:
| Your account already has | What `boat onboard` does |
| - | - |
| An active plan | Prints your plan and usage. **No Stripe Checkout**, no second subscription. |
| A plan with depleted usage | Points you at the billing dashboard to top up or change tier, instead of starting another plan. |
| Answered what your sandboxes are for | Does not ask that question again, whichever way you answered. |
| A past subscription | Offers checkout without promising a 7-day trial, which Stripe grants once per customer. |
Boat records your answer on the environment when you give it. Boat keeps both answers, not only "a platform of mine". A plan never counts as an answer, because the dashboard checkout does not ask the question.
See [Reinstalling, or a second machine](/account#reinstalling-or-a-second-machine).
### `boat login [key]`
Sign in with an API token that you already have. Without a key, the command starts a sign-in in the browser. GitHub is the default. Google and email also work.
```bash theme={null}
boat login
boat login --google
boat login --email you@example.com
boat login --key-stdin <<< "$BOAT_API_KEY"
boat login boat_abc123...
```
`--key-stdin` reads the token from standard input, not from the command line. Other users on the host can read the process arguments, so the token stays out of them. Use `--key-stdin` when you sign in without a person present. You can also give the token as an argument. You cannot use both. `--key-stdin` needs a Boat CLI newer than 0.1.211.
Without a key, the CLI prints a URL and opens it. Then it waits up to six minutes for you to finish in the browser. `--email` sends a code to that address. If you omit the address, the browser page asks for it. Boat refuses disposable email domains.
All methods sign in to the same account only if they are already **connected** to it. A method that you never used before starts a new account, even when the email is the same. Add connections in [Dashboard > Account](https://boat.dev/dashboard?tab=account) instead. See [Quickstart](/account#sign-in-methods).
To create production API keys, run `boat api-key create` or use the API Keys page of the dashboard. For key management, see [API Keys](/api-keys). For Docker and CI setup, see [In scripts and CI](#in-scripts-and-ci).
### `boat api-key`
Create and manage API keys for SDKs, CI, and other projects.
```bash theme={null}
boat api-key create my-project --ttl 90d --preset ci
boat api-key create my-project --ttl 7d --sandbox bx_123 --actions sandbox.read,exec
boat api-key list # scope, expiry, last used
boat api-key list --all # also platform-managed per-sandbox machine keys
boat api-key usage # 30-day requests and live resource count
boat api-key usage --verbose # plus sandboxes/Agents split and created list
boat api-key rotate # old secret stops working, new one shown once
boat api-key revoke
```
| Flag | Rule |
| - | - |
| `--ttl` | Default 90d. Maximum 365d. |
| `--sandbox`, `--env` | You can repeat them. |
| `--preset` | `read-only`, `full-sandbox`, `ci`, or `admin`. |
| `--actions` | You cannot use it with `--preset`. |
`create` and `rotate` show the secret one time. You cannot get it again later. `list` shows the prefix, the last four characters, the scope, the expiry, the 30-day request total, and the live resource count. `usage` still works after you revoke a key. Revoke does not delete sandboxes or Agents.
`create` calls the `POST /api/v1/api-keys/scoped` endpoint. It needs a browser session or a token with admin scope. During a rollout, `create` can be unavailable for a short time. `api-key list` stays available. `rotate` and `revoke` still need a browser session. See [API Keys](/api-keys).
### `boat webhook`
Register lifecycle webhooks for all sandboxes of your account.
```bash theme={null}
boat webhook create https://example.com/hooks/boat --event ready --event error
boat webhook list
boat webhook rotate
boat webhook remove
```
The events are `sandbox.ready`, `sandbox.error`, `sandbox.archived`, `sandbox.hydrated`, `sandbox.degraded`, and `sandbox.recovered`. To subscribe to all of them, omit `--event`. You see the signing secret only when you create or rotate the webhook. See [Webhooks](/webhooks).
### `boat telemetry`
Send sandbox and account metrics, and lifecycle events, to an OTLP/HTTP endpoint.
```bash theme={null}
boat telemetry # show the export and its last result
boat telemetry set https://otlp.datadoghq.com -H dd-api-key=$DD_API_KEY
boat telemetry unset
```
`set` replaces the current export and sends a test export immediately. For more headers, repeat `-H key=value`. Boat encrypts the header values and never shows them. See [OpenTelemetry](/opentelemetry).
### `boat logout`
Sign out and clear the local token.
```bash theme={null}
boat logout
```
### `boat status`
Show the API health, the account that is signed in, its plan, and the path of the local configuration.
```bash theme={null}
boat status
```
***
## Sandbox lifecycle
### `boat new`
Create a new sandbox.
```bash theme={null}
boat new [--ttl SECONDS]
boat new --type small
boat new --type large
boat new --no-auto-stop
boat new --env KEY=VALUE --env OTHER=VALUE
boat new --no-env
boat new --no-snapshots
boat new --no-monitoring
boat new --fail-fast
boat new --setup-file ./setup.sh
boat new --environment users
boat new --from web-stack
boat --org acme new
boat org switch acme
boat new
boat new --personal
```
| Flag | Default | Maximum |
| - | - | - |
| `--type` | `default` | `small` or `large` |
| `--ttl` | `3600` (1 hour) | `2592000` (30 days) |
| `--no-auto-stop` | off | no automatic stop |
| `--env`, `-e` | none | 100 variables, 64KB total |
| `--environment` | your default (`base`) | one named environment |
| `--no-env` | off | none |
| `--no-snapshots` | off | none |
| `--no-monitoring` | off | none |
| `--fail-fast` | off | none |
| `--setup-file` | none | 64KB script file |
| `--from` | none | one named snapshot or share code |
| `--personal` | off | bills you, not the active org |
`--type` sets the machine size. A fork gets the size of its source sandbox, unless you pass `--type`. `boat resume --type` moves a sandbox to a different size. For the specifications and the hourly price of each size, see [Pricing](/pricing).
On the free trial, `--no-auto-stop` and a `--ttl` above 2 hours are refused, and `--type large` is not available. See [Pricing](/pricing#concurrency).
`--env` sets environment variables on this sandbox only, on top of your dashboard secrets. When two variables have the same name, the value for this sandbox wins. See [Environments](/environments).
`--environment ` sets the environment that the sandbox starts from: its repositories, secrets, and credentials. If you omit it, the sandbox uses your default. Boat refuses an unknown name before it creates the sandbox, so a typo costs you nothing. `--environment` and `--env` are not related. The first selects a template. The second sets a variable on this one sandbox. See [Environments](/environments).
`--no-env` creates a sandbox that gets none of the secrets of your account. The sandbox also cannot act on your account or on other sandboxes. Use it for sandboxes that you give to your own users. SSH, SCP, desktop, snapshots, and public URLs still work. See [Environments](/environments). If you make many such sandboxes, use an environment marked safe for third parties instead. It applies the same protection to each sandbox that uses it, and the protection stays after forks and resumes.
`--no-snapshots` creates a sandbox that Boat never snapshots. When you stop it, Boat erases it. See [Snapshots off](/data-retention/snapshots-and-monitoring-off#snapshots-off).
`--no-monitoring` creates a sandbox for which Boat keeps none of these: command output, process output, the process list, CPU and RAM samples. You still see the live output of a running command. See [Monitoring off](/data-retention/snapshots-and-monitoring-off#monitoring-off).
`--fail-fast` creates the sandbox only if a machine of its type is ready now. It answers in about 1.5 seconds. If no machine is ready, it fails with `no_ready_machine`. Boat then creates nothing, bills nothing, and counts nothing against your start limits. It works with `--from`. `boat fork` and `boat resume` also accept it. See [Fail fast](/machines/fail-fast).
`--setup-file` reads a local shell script (UTF-8, up to 64KB). Boat runs the script on the sandbox in the background after the sandbox is ready. The script never delays `ready`. To see the result, read `setupStatus` (`pending`, `running`, `done`, `failed`) and `setupError` in `boat info`. You can use it with `--from`.
`--personal` bills you, also when an org wallet is active. For daily use, run `boat org switch` one time. All later `boat new` commands use that wallet. To change the wallet for one create, run `boat --org new` or `boat --org personal new`. See [`boat org`](#boat-org).
`--from ` starts the sandbox from a named snapshot that you saved with `boat snapshot `. The stack is then already installed. The sandbox does not get the environment of the source sandbox. Pass `--environment`, or the sandbox uses your default. This flag is not related to `boat env set-file --from`, which reads a local file. See [Snapshots & Copies](/snapshots).
`--from bsh_...` uses a share code that another account gave you. The new sandbox is yours and bills your account. It does not get the per-sandbox variables of the owner. See [Share a snapshot with another account](/snapshots/templates#share-a-template-with-another-account).
The command returns the sandbox ID, the IP, and the first state.
For a long workflow without interruptions, disable auto-stop:
```bash theme={null}
boat new --no-auto-stop
```
See [Long-Running Tasks](/long-running-tasks).
### `boat list`
List your sandboxes that are up or running, with their current state and ID. `boat list` without flags is the same as `boat list --filter r`.
```bash theme={null}
boat list
boat list --filter s
boat list --filter sr
boat list --all
```
Give `--filter` one or more state group letters:
| Filter | Includes |
| - | - |
| `r` | Up/running sandboxes: `ready`, `cloning`, `idle`, `running` |
| `s` | Stopped sandboxes: `stopped` |
| `p` | Pending sandboxes: `init`, `provisioning` |
| `t` | Stopping sandboxes: `stopping` |
| `e` | Error sandboxes: `error` |
You can combine letters. For example, `boat list --filter sr` lists stopped sandboxes and sandboxes that are up or running. To include all states, use `--all`.
### `boat extend `
Change the auto-stop timer of a sandbox.
```bash theme={null}
boat extend bx_f7k2q9hd --hours 12
boat extend bx_f7k2q9hd --ttl 2592000
boat extend bx_f7k2q9hd --no-auto-stop
```
To extend by a fixed time, use `--hours` or `--ttl`. To keep the sandbox running until you stop it, use `--no-auto-stop`.
### `boat info `
Show the details of one sandbox: state, IP, desktop availability, and remaining TTL.
```bash theme={null}
boat info bx_f7k2q9hd
```
### `boat stop `
Pause a running sandbox. Boat makes a snapshot, then stops billing. The sandbox goes to `archiving`, then to `archived`.
```bash theme={null}
boat stop bx_f7k2q9hd
```
The snapshot takes a moment. When the command returns, the sandbox is not stopped yet. Poll `boat info` until the state is `archived`.
The stop prints its id (`operation.id`, for example `stop_3f9a1c2e7b4d5a60`). To read that stop later, pass the id to `--status`. The status is `pending`, `failing`, `completed` or `superseded`. `superseded` means that a resume ended the stop. You can still read old stops after a resume or a later stop.
```bash theme={null}
boat stop bx_f7k2q9hd --status stop_3f9a1c2e7b4d5a60
```
**If Boat refuses a stop.** A stop saves the disk first. If that save fails, Boat does these things:
1. It refuses the stop and keeps the sandbox running, so your work is not lost.
2. It tries the save again automatically.
3. It sends you an email.
**Boat does not bill you for the time that your sandbox spends in that state.** The meter pauses automatically from the first failed attempt. See [When a stop is refused](/snapshots/stop#when-a-stop-is-refused).
A refused stop stays pending until a save succeeds. Then the sandbox stops automatically.
`--force` stops the sandbox now and keeps its last successful snapshot. All data written after that snapshot is lost permanently. Use it only after a stop failed.
```bash theme={null}
boat stop bx_f7k2q9hd --force
```
`--force` discards unsaved work. First, read the time of the last snapshot in `boat info`. Then you know exactly what you lose.
### `boat delete `
Permanently delete a sandbox **and its snapshots**. Boat force-stops the sandbox without a last snapshot. Then Boat deletes from storage each snapshot chain that only this sandbox owns.
```bash theme={null}
boat delete bx_f7k2q9hd
boat delete bx_f7k2q9hd --yes
boat delete self --yes # from inside a sandbox
```
`boat delete` destroys data. You cannot resume, fork or recover a deleted sandbox. No list of deleted sandboxes exists to restore from.
**If you want to use the data later, run `boat stop` instead.** A stopped sandbox is free and keeps its disk. `boat resume` brings it back in the same condition.
The command asks you to confirm. To skip the question, pass `--yes`. In scripts (`--json`, or no terminal attached), `--yes` is necessary. Without it, the command refuses and does not wait.
Deletion is not instant. Boat must remove the machine before it can remove the data. The command does these steps:
1. It removes the sandbox from `boat list` immediately.
2. It prints the deletion operation id (`bdop_…`).
3. It follows that operation until it finishes.
**What Boat keeps.** Boat keeps snapshot data that something else also uses. If Boat deleted it, the other user of the data would break:
* A sandbox that you **forked** from this one, or a **resume** of it, restores from the same physical snapshot objects.
* A **named snapshot** saved from this sandbox (`boat snapshot `) keeps its data. To remove those bytes too, run `boat snapshot rm `.
Boat releases the storage when the last sandbox or named snapshot that uses a chain is gone.
### `boat deletion status `
Read the status of a deletion that you started earlier, here or through the API. The status is `pending`, `processing`, `blocked` or `completed`. `blocked` is not a final state. It means that an attempt found something that it cannot finish yet. Usually, another sandbox still reads a snapshot. Boat tries again automatically, with backoff.
```bash theme={null}
boat deletion status bdop_0123456789abcdef0123456789abcdef
```
See [Data retention and deletion](/data-retention).
### `boat resume `
Resume a stopped sandbox from its last snapshot. Before the CLI prints its last `ready` event, it waits for two things:
1. A ready state.
2. A test command that does nothing and succeeds.
This shows that the sandbox can run commands. The CLI does not wait for background hydration or setup scripts. The wait stops after 30 minutes. It fails on a final sandbox state or on an API error that cannot be retried. Older Box and Boat binaries need an updated CLI to get this check. See [Snapshots](/snapshots).
```bash theme={null}
boat resume bx_f7k2q9hd
boat resume --no-env bx_f7k2q9hd
boat resume --type large bx_f7k2q9hd
boat resume -e KEY=VALUE bx_f7k2q9hd
boat resume --ttl 7200 bx_f7k2q9hd
boat resume --no-auto-stop bx_f7k2q9hd
```
The sandbox must have a completed snapshot. This means that you stopped it correctly with `boat stop`.
| Flag | What it does |
| - | - |
| `-e`, `--env` | Sets per-sandbox environment variables. It works the same as `boat new -e`. Repeat it for more values. A per-sandbox value replaces a dashboard variable with the same name. The set you pass **replaces** the current per-sandbox variables of the sandbox. |
| `--environment ` | Moves the sandbox to a different environment when it resumes. |
| `--no-env` | Removes your account secrets, and removes owner secrets that came from the snapshot. You cannot undo this. The sandbox stays no-env. |
| `--type` | Resumes on a different machine size: `small`, `default`, or `large`. |
| `--ttl ` | Sets the lifetime of the resumed sandbox. |
| `--no-auto-stop` | Turns auto-stop off. |
Without `--environment`, the sandbox keeps the environment version that it had. A resume never moves a sandbox to a newer version without your command. Boat refuses an unknown name before it changes anything. The new environment can withhold a secret that the sandbox has now. Boat then removes that secret from the disk during the resume, and it does not come back. See [Environments](/environments).
Without `--type`, the sandbox keeps its current size. Boat refuses to shrink a sandbox that has more data than the smaller machine can hold. The sandbox then stays as it was. See [Machine Capabilities](/machines).
Without `--ttl` and `--no-auto-stop`, the sandbox keeps its current setting. A resume never makes the lifetime shorter or longer without your command.
A resume is like a server reboot. The systemd services that you enabled start again automatically. Processes that you started by hand do not come back: dev servers, background jobs, tunnels, desktop sessions. Start them again, or make them systemd services.
### `boat recover `
Power-cycle the machine of a sandbox that does not respond. Boat does not touch the disk. Usually the sandbox answers again in 1-2 minutes.
Boat refuses a recover in these cases. Resume the sandbox instead.
* The sandbox answers normally.
* A recover started less than 10 minutes ago.
* Boat cannot power-cycle its machine.
This command does the same as `POST /sandboxes/{id}/recover` and Recover in the dashboard.
```bash theme={null}
boat recover bx_f7k2q9hd
```
### `boat fork `
Copy a sandbox from its latest snapshot into a new, independent sandbox.
```bash theme={null}
boat fork bx_f7k2q9hd
boat fork --no-env bx_f7k2q9hd
boat fork --type small bx_f7k2q9hd
boat fork -e TENANT_ID=acme bx_f7k2q9hd
boat fork --ttl 600 bx_f7k2q9hd
boat fork --no-auto-stop bx_f7k2q9hd
```
The command returns the new sandbox ID asynchronously (HTTP 202). The source sandbox must have a completed snapshot.
A fork copies the filesystem of the snapshot into a new sandbox. The systemd services that you enabled start automatically, as on a resume. Processes that the source sandbox started by hand are not in the fork. Start them again in the fork if necessary.
Boat never changes the source sandbox.
| Flag | What it does |
| - | - |
| `--type` | Gives the fork a machine size that is different from its source. |
| `--ttl `, `--no-auto-stop` | Set the lifetime of the fork. |
| `--environment ` | Puts the fork on a different environment. |
| `--no-env` | Makes the fork a no-env sandbox. |
| `-e`, `--env` | Sets per-sandbox environment variables on the fork. It works the same as `boat new -e`. |
A fork does **not** get the lifetime of its source. The default is 1 hour. So a fork of a sandbox with auto-stop off does not make another sandbox that nothing stops.
Without `--environment`, the fork gets exactly the environment version of its source. A fork never gets configuration that its source did not have. See [Environments](/environments).
A fork of a no-env source is always no-env.
The variables that you pass with `--env` **replace** the per-sandbox variables that the fork gets from the source sandbox. Without `--env`, the fork gets them unchanged.
### `boat events `
Read the work of the agent on a sandbox: prompts and responses. By default, the command streams **all conversations** on the sandbox. Each event has a `conversationId`, which you see with `--json`. To keep polling for new events, add `--follow`. To watch one conversation, add `--convo`.
These events are the conversations of the agent, not the lifecycle of the sandbox. A sandbox that never got a prompt returns an empty list, also after it started, stopped and resumed. For the lifecycle state, use `boat info`.
```bash theme={null}
boat events bx_f7k2q9hd
boat events bx_f7k2q9hd --follow
boat events bx_f7k2q9hd --convo # just one conversation
boat events bx_f7k2q9hd --convo current # this shell's current conversation
```
| Flag | Description |
| - | - |
| `--follow` | Continue polling and printing new events until interrupted |
| `--convo ` | Only stream this conversation (or `current`). Repeat for several. Omit to stream all conversations. Alias `--conversation` |
### `boat conversations [id]`
List the conversations of a sandbox. The conversation with the most recent prompt comes first. Each row shows these values:
* The id.
* The last activity.
* The number of prompts.
* If a turn is running.
* The last harness and model.
* A preview of the last prompt.
Alias: `sandbox convos`.
```bash theme={null}
boat conversations
sandbox convos bx_f7k2q9hd
boat conversations bx_f7k2q9hd --json # one JSON line per conversation
```
```
ID When Prompts Running Harness/model Last prompt
8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c just now 1 yes pi/claude-sonnet-5 Investigate the flaky CI job [sandbox current, this shell]
2c0d9e4b-7a1f-4c3e-8b5d-6e7f8a9b0c1d 4 mins ago 2 no claude Now add tests
```
Two markers can come after the last prompt:
| Marker | Conversation |
| - | - |
| `sandbox current` | The one that `boat prompt` without flags continues from any shell. This is the conversation of the sandbox with the most recent prompt. |
| `this shell` | The one that `boat prompt` without flags continues from this shell. |
Copy an id into one of these commands:
* `boat prompt --resume `
* `boat steer --convo `
* `boat events --convo `
* `boat interrupt --convo `
### `boat steer `
Send a message to a turn that is **already running**, and do not stop it. The agent uses the message and continues its work. There are three options:
| Command | What happens to the running turn |
| - | - |
| `boat prompt` | Waits for the turn to end. |
| `boat steer` | Adds your message to the turn. |
| `boat interrupt` | Discards the turn. |
```bash theme={null}
boat steer "Skip the integration tests, the unit tests are enough"
boat steer bx_f7k2q9hd "Also update the changelog when you are done"
boat steer bx_f7k2q9hd --convo "Focus on the auth module first"
boat steer --convo current "Stop after the first failure"
```
Without `--convo`, the command steers the conversation that this shell prompted last. This is the same conversation that `boat prompt` without flags continues. If the conversation has no running turn, Boat refuses the steer with `no_running_turn`. Use `boat prompt` in that case.
How each harness handles a steer:
| Harness | What happens |
| - | - |
| Claude Code, Codex, pi, Prime Agent | The harness adds the message to the running turn natively. Nothing is interrupted. If the harness accepts the message but the turn ends before it uses the message, Boat immediately continues with a new turn on the same session. So the agent always acts on a steer. |
| OpenCode | OpenCode cannot take a message during a turn. Boat stops that turn and immediately continues the same conversation with your instruction. The conversation keeps all of its memory. |
The `native` field of the JSON output tells which path ran. The steer shows in `boat events` as a `steer` event with the final `mode`: `native`, `native-continued`, `fallback` or `late`. See [Integrated agents](/integrated-agents).
| Flag | Description |
| - | - |
| `--convo ` | Steer this conversation (or `current`). Omit to steer the one this shell last prompted. Alias `--conversation` |
### `boat interrupt `
Interrupt the running agent work in a sandbox. By default, the command stops **all conversations** on the sandbox. To stop only one, use `--convo`. The other conversations keep running.
```bash theme={null}
boat interrupt bx_f7k2q9hd
boat interrupt bx_f7k2q9hd --convo # stop just one conversation
boat interrupt bx_f7k2q9hd --convo current
```
| Flag | Description |
| - | - |
| `--convo ` | Interrupt only this conversation (or `current`). Omit to interrupt the whole sandbox. Alias `--conversation` |
***
## Access
### `boat ssh [command]`
Open an SSH session in a sandbox, or run one command without a session. The CLI manages the key at `~/.ssh/ascii_box_ed25519` automatically. The box CLI used the same key file, so you do not authorize it again.
```bash 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
```
### `boat exec [command...]`
Run a command in a sandbox through the Boat API. You do not need an SSH session or a key. `boat exec` exits with the exit code of the remote command, so you can use it directly in scripts and CI.
```bash theme={null}
boat exec bx_f7k2q9hd "npm test"
boat exec bx_f7k2q9hd --cwd my-repo --timeout 120 -- npm run build
boat exec bx_f7k2q9hd bash -lc 'echo "$HOME" && ls'
```
One argument runs as a shell command line. More than one argument runs as argv. Each argument keeps its quoting, as with `boat ssh`.
| Flag | Default | Description |
| - | - | - |
| `--cwd ` | sandbox work directory | Working directory, relative to the sandbox work directory (`/home/user`) |
| `--timeout ` | `30` | Synchronous timeout, 1–600 seconds |
| `--detach` | off | Run in the background and print the process id |
| `--status ` | none | Show status and logs of a process started with `--detach` |
| `--kill ` | none | Stop a process started with `--detach`, and every process it started |
| `--attach ` | none | Print the output of a process started with `--detach` as it writes it, from its start, until it ends. Exits with its exit code. Reconnects every 10 minutes with no gap |
Wait until the sandbox is `ready` before you run `boat exec`. Boat refuses earlier calls with a `boat_starting` error that you can retry.
The synchronous limit is 600 seconds. For a command that can run longer, detach it and poll:
```bash theme={null}
pid="$(boat exec bx_f7k2q9hd --detach "npm run build" | jq -r .processId)"
boat exec bx_f7k2q9hd --status "$pid"
boat exec bx_f7k2q9hd --attach "$pid" # follow the output live until it ends
```
`--detach` starts the command in the background and returns a JSON object. Take `processId` from it, as in the example. When you pipe or capture the output, `--json` turns on automatically. So in a script you always get JSON here, also without the flag.
The output goes to `~/.ascii/processes/.log` on the sandbox. `--status ` prints these values:
* If the process still runs.
* The exit code, after the process ends.
* The last lines of stdout and stderr.
If the agent of the sandbox restarts (stop and resume), the status changes to `lost`. The log files stay on the disk. See [Long-Running Tasks](/long-running-tasks).
### `boat history [command-id]`
List the commands that ran in a sandbox, newest first. With a command id, print the stored output of that command. This works while the sandbox is stopped.
| What | Boat keeps it for |
| - | - |
| Commands run through `boat exec` or the API | 90 days |
| The output of all other processes in the sandbox | 72 hours |
```bash theme={null}
boat history bx_f7k2q9hd
boat history bx_f7k2q9hd --mode commands
boat history bx_f7k2q9hd cmd_k3v9x2mq7tbw --stderr
boat history bx_f7k2q9hd cmd_k3v9x2mq7tbw --follow
```
| Flag | Default | Description |
| - | - | - |
| `--limit ` | `20` | How many commands to list, at most 100 |
| `--mode ` | all | Only these, comma-separated: `commands` (what you ran), `process` (every other process), or `sync`, `stream`, `detached` |
| `--stderr` | off | Print stderr instead of stdout |
| `-f`, `--follow` | off | Keep printing new output until the command ends |
See [Command History](/long-running-tasks/command-history).
### `boat ps `
List the processes in a sandbox, busiest first: pid, CPU, memory, user, history record and command line. To read the output of a process, pass its HISTORY id to `boat history`. For a stopped sandbox, the command shows the last sample that Boat kept, with its time.
### `boat env`
Manage the templates that new sandboxes start from: repositories, secrets, and the credentials that a sandbox can use. Each change makes a new version that you cannot change. Running sandboxes stay on their start version until you run `boat env upgrade`. See [Environments](/environments).
`--environment` and `--env` are different. `boat new --environment staging` sets the environment that the sandbox uses. `boat new --env KEY=value` sets one variable on that one sandbox, on top of what the environment gives it.
```bash theme={null}
boat env list
boat env info base
boat env new staging
boat env rename staging prod
boat env default prod
boat env rm staging
```
| Command | Description |
| - | - |
| `boat env list` | Your environments, with the default marker, latest version, and live sandboxes per version |
| `boat env info ` | One environment in full: toggles, variables, secret files, repos, versions |
| `boat env new ` | Create an environment. Starts with everything injected |
| `boat env rename ` | Rename. Sandboxes stay pinned to their versions |
| `boat env default ` | Make it the environment new sandboxes use when none is named |
| `boat env rm ` | Soft delete. Pinned sandboxes keep running. New sandboxes cannot use it |
#### `boat env set `
Change what the environment injects.
```bash theme={null}
boat env set base --safe-for-third-parties true
boat env set base --github true --secrets false
```
| Flag | Description |
| - | - |
| `--safe-for-third-parties ` | Master switch. `true` injects nothing of yours and overrides every flag below |
| `--github ` | Your GitHub access: token and repository clones |
| `--secrets ` | Your environment variables and secret files |
| `--sandbox-credentials ` | The in-sandbox Boat CLI credentials, so the sandbox can manage its own lifecycle |
| `--agents-credentials ` | Your agent provider logins (Claude, Codex, and so on) |
| `--credential-profile ` | The [credential profile](/ai-credentials#credential-profiles) the sandboxes get. `default` = the sandbox owner's default profile |
| `--proxy-ai-credentials ` | With `--safe-for-third-parties true`: still pass AI credentials, as proxy placeholders. Needs `boat ai proxy on` for the profile owner |
#### Organization environments
Add `--org ` to any `boat env` command to work on the organization's shared environments. Members read and use them. Owners and admins change them. Without `--org`, `boat env` works on your own environments. See [Environments](/environments/named-environments#organization-environments).
```bash theme={null}
boat env list --org acme
boat env set agents --org acme --credential-profile team-claude
```
#### Contents
```bash theme={null}
boat env set-var base STRIPE_KEY=sk_live_123
boat env rm-var base STRIPE_KEY
boat env set-file base backend/.env --from ./local.env
cat ./local.env | boat env set-file base backend/.env
boat env rm-file base backend/.env
boat env add-repo base octocat/hello-world --branch develop
boat env rm-repo base octocat/hello-world
```
`set-file` reads the contents from `--from `. Without `--from`, it reads from stdin. Paths are relative to the sandbox work directory (`/home/user`).
#### `boat env upgrade `
Move the sandboxes of this environment to its latest version.
* Live sandboxes get the new configuration immediately. First, Boat removes from the machine each secret that the new version withholds.
* Stopped sandboxes get it when they resume.
* Nothing upgrades without this command.
```bash theme={null}
boat env upgrade base
```
Boat deletes a withheld secret from the disk of the sandbox. It does not hide it. If you pin the sandbox to the older version again, the secret does not come back.
### `boat host `
Expose a service that runs in a sandbox on a stable HTTPS URL. You do not open an SSH session.
```bash theme={null}
boat host bx_f7k2q9hd 3000
boat host bx_f7k2q9hd 3000 --title "Login preview"
boat host bx_f7k2q9hd 3000 --public
boat host bx_f7k2q9hd 5173 --localhost
boat host bx_f7k2q9hd 3000 --json
```
The command opens the sandbox firewall, registers the HTTPS subdomain, and prints the URL. If you run it again for the same sandbox and port, you get the same URL.
| Flag | Default | Description |
| - | - | - |
| `--title ` | none | Set the display title for the hosted port. |
| `--private` | on | Require the generated `_token` query parameter to access the URL. |
| `--public` | off | Clear any saved access token and return a URL that does not require `_token`. |
| `--localhost` | off | The service listens on `localhost`, `127.0.0.1` or `::1` only. The sandbox relays the URL to it. Leave it off for a service on `0.0.0.0`. See [Services that listen on localhost](/hosting/how-it-works#services-that-listen-on-localhost). |
### `boat scp`
Copy files to or from a sandbox. Use `bx_:/path` as the remote address.
```bash theme={null}
# Download a file
boat scp bx_f7k2q9hd:/home/user/output.zip ./output.zip
# Upload a file
boat scp ./local-file.txt bx_f7k2q9hd:/home/user/
# Copy a directory recursively
boat scp --recursive ./my-project bx_f7k2q9hd:/home/user/
```
| Flag | Description |
| - | - |
| `--recursive` | Copy directories recursively |
On macOS and Linux, a recursive copy to a new directory can send one archive over SSH. This needs `tar` on both ends. It removes one network round trip for each file. You see the new directory only after the full transfer. These copies use the installed OpenSSH `scp`:
* Single files.
* Merges into a directory that exists.
* Trees that the archive mode does not support.
### `boat forward `
Forward one TCP port from a sandbox to your computer.
```bash theme={null}
boat forward bx_f7k2q9hd --remote 8080
boat forward bx_f7k2q9hd --remote 3000 --bind 0.0.0.0
```
| Flag | Default | Description |
| - | - | - |
| `--remote ` | required | Remote port on the sandbox |
| `--local ` | same as remote | Local port to bind |
| `--bind ` | `127.0.0.1` | Local bind address |
#### `--reverse`: reach your machine from the sandbox
`--reverse` sends the tunnel in the other direction. A port on your computer
becomes available inside the sandbox at `127.0.0.1:`. An agent in the
sandbox can then call an MCP server, a local model, or a webhook receiver that
stays on your laptop.
```bash theme={null}
# your app on localhost:7777 answers at 127.0.0.1:7777 inside the sandbox
boat forward bx_f7k2q9hd --reverse --local 7777
# a local model on 11434 shows up inside the sandbox on 11500
boat forward bx_f7k2q9hd --reverse --local 11434 --remote 11500
```
| Flag | Default | Description |
| - | - | - |
| `--local ` | required | Port on your computer to expose |
| `--remote ` | same as local | Port the sandbox listens on |
| `--bind ` | `127.0.0.1` | Bind address **inside the sandbox**. Anything other than loopback needs `GatewayPorts` enabled on the sandbox's sshd |
Both directions run in the foreground until you press Ctrl+C. The tunnel can
stop because the sandbox moved. For example, `boat stop` then `boat resume`
brings the sandbox back on a new IP. The CLI then gets the connection again and
tries up to five times before it stops. A failure at startup, for example a
port that is already in use on the other side, shows immediately. The CLI does
not try again.
Neither direction makes anything public. The tunnel uses the same SSH session
as `boat ssh`, and the sandbox side listens on loopback only. For a public HTTPS
URL for a port inside the sandbox, use [`boat host`](#boat-host-id-port).
### `boat desktop `
Open the desktop streaming URL of the sandbox in your browser.
```bash theme={null}
boat desktop bx_f7k2q9hd
boat desktop bx_f7k2q9hd --vnc
boat desktop bx_f7k2q9hd --vnc --public
```
By default, the desktop streams over Moonlight (60fps WebRTC). Some networks
block traffic or have low bandwidth. There, WebRTC can be unstable or not
connect. Use `--vnc` in that case. The VNC stream goes over plain HTTPS and
works much better on poor connections. If the default stream has problems, the
viewer also has a one-click switch to VNC.
| Flag | Default | Description |
| - | - | - |
| `--vnc` | off | Stream over VNC instead of Moonlight. More stable on poor networks. |
| `--public` | off | With `--vnc`, return a URL that does not require an access token. |
The first `--vnc` on a sandbox takes a few seconds, because the sandbox
prepares the VNC stack. The CLI shows a spinner. The next opens are instant.
### `boat browser `
Get a stream that shows only the browser of the running sandbox.
```bash theme={null}
boat browser bx_f7k2q9hd
boat browser bx_f7k2q9hd --profile /data/chrome-profile
```
| Flag | Default | Description |
| - | - | - |
| `--profile ` | a managed dir on the sandbox | Absolute path on the sandbox used as the Chrome user data dir (the Chrome profile). |
With `--profile`, you can keep more than one signed-in Chrome profile on one
sandbox. You select one profile for each stream. Boat creates the directory and
gives it to the browser user. The path must be absolute and outside system
directories. It must not be under `/home/user`, because the browser sandbox
hides that path with an empty directory. The profile is on the sandbox
filesystem, so snapshots and forks keep it. When you change the profile, Chrome
restarts and the stream stays up. Sandboxes created before this feature return
`chrome_profile_unsupported`. To use the flag, create the sandbox again on a
current image.
The stream has these properties:
* 1920x1080 at 60 fps.
* It shows Chrome with tabs and an address bar.
* It uses a separate streaming process and identity.
* It does not give access to the desktop clipboard bridge.
* It does not create a separate sandbox.
* It cannot fall back to the desktop or VNC.
* The window-close controls are hidden. Mute, reconnect, and fullscreen stay on the viewer.
Behavior:
* When you close the last tab, Chrome opens a new tab and the stream stays up.
* When you run the command again, the live stream stays up.
* When you open the new URL in a second tab, it replaces the first tab and keeps the same Chrome session.
* New sandboxes get the browser-view files when Boat provisions them.
Keep the URL secret.
This feature limits the display and the input. It does not isolate the browser from the sandbox. Code that already runs in the sandbox can read or change the browser process and its data. This is especially true for code with root access.
***
## Snapshots
### `boat snapshots [id]`
List the snapshots of all your sandboxes, or of one sandbox.
```bash theme={null}
boat snapshots
boat snapshots bx_f7k2q9hd
```
### `boat snapshot `
Save the disk of a sandbox under a name. `boat new --from ` can then deploy it as many times as you want. To replace a named snapshot, use its name again.
```bash theme={null}
boat snapshot bx_f7k2q9hd web-stack
boat snapshot current web-stack # from inside a sandbox
boat snapshot rm web-stack # remove it and release its storage
```
A save from a running sandbox first makes a new capture, so it can take minutes. The CLI polls until the snapshot is `ready`. You can keep up to 10 named snapshots. See [Snapshots & Copies](/snapshots).
### `boat snapshot share|shares|unshare`
Give another Boat account a secret code that deploys one of your named snapshots.
```bash theme={null}
boat snapshot share web-stack # make a code; it shows once
boat snapshot shares web-stack # list codes, use counts, status
boat snapshot unshare web-stack shr_... # revoke one code
boat new --from bsh_... # the other account deploys it
```
`share` prints the full code one time only. Save it immediately. Boat removes these items from the sandbox of the other account: your logins, API keys, secret files, repository clones and SSH keys. The other account gets all other data on the disk. See [Share a snapshot with another account](/snapshots/templates#share-a-template-with-another-account).
### `boat snapshot latest|tree|pull`
Look inside a snapshot or download it. This works while the sandbox is stopped.
```bash theme={null}
boat snapshot latest bx_f7k2q9hd
boat snapshot tree # files and sizes
boat snapshot pull -o ./restore # download and reassemble
```
`pull` writes `home_user/` (your `/home/user`) and `docker/` (named volumes). See [Snapshots](/snapshots).
### `boat snapshot delete ...`
Permanently delete ordinary filesystem snapshots. The command accepts all snapshots, and the snapshot disappears immediately. Boat erases its stored data in the background when nothing reads it. A newer snapshot that you keep can be built on it. Then its data stays until that newer snapshot is deleted too. `--all ` deletes all ordinary snapshots of a sandbox and keeps the sandbox. To remove a named snapshot, use `boat snapshot rm ` instead.
```bash theme={null}
boat snapshot delete
boat snapshot delete --yes
boat snapshot delete --all bx_f7k2q9hd
```
See [Data retention and deletion](/data-retention).
***
## AI agents
### `boat prompt [id]`
Send a prompt in natural language to an AI agent in the sandbox.
* Without an ID, the prompt goes to the current sandbox of your shell (the last `boat new` there). If you run the CLI inside a sandbox, it goes to that sandbox.
* Without `--provider`, the prompt uses the default harness and model from the Agents page of the dashboard.
```bash theme={null}
boat prompt "Fix the failing tests in this repo"
boat prompt bx_f7k2q9hd --provider claude "Fix the failing tests in this repo"
boat prompt bx_f7k2q9hd --provider codex --model gpt-5.4-mini --reasoning-effort xhigh "Refactor auth module"
boat prompt bx_f7k2q9hd --provider codex --model gpt-6.1-sol --fast "Refactor auth module"
boat prompt --attach ./error.png --attach ./trace.log "Why does this crash?"
```
| Flag | Required | Values |
| - | - | - |
| `--provider` | no | `claude`, `codex`, `pi`, `opencode`, `prime`, `kimi`, `mistral`. Default: the Agents page default harness |
| `--model` | no | Provider-specific model name. Default: the model selected for that harness on the Agents page |
| `--reasoning-effort` | no | Provider/model-specific thinking level such as `none`, `low`, `medium`, `high`, `xhigh`, or `max`. The accepted levels differ for each model. Some models have none |
| `--fast` | no | No value. Run this prompt in fast mode: same model, faster output, billed by the provider at its higher fast rate. Only fast-capable models accept it. Default: the choice saved on the Agents page for that model |
| `--new` | no | Start a new conversation instead of continuing this shell's current one |
| `--resume ` | no | Continue a specific conversation by id (or `current` for this shell's) |
| `--attach ` | no | Attach a file (image, PDF, any file). Repeat for multiple. Alias `--image`. Saved on the sandbox under `~/attachments`. Images are also sent to vision-capable models |
To get the current list of providers, models, and reasoning efforts from the Boat API, run `boat prompt --help`. It lists the models that support fast mode under "Fast mode (--fast):".
#### Conversations (running agents in parallel)
A sandbox runs **many conversations at the same time**. Each conversation has its own history. So one sandbox can run more than one independent agent task in parallel, for example different features, different users, or a demo. The tasks do not wait in a queue.
```bash theme={null}
boat prompt --new "Start on the billing refactor" # conversation A
boat prompt --new "Investigate the flaky test" # conversation B, runs alongside A
boat prompt --resume "Now add tests" # continue a specific conversation
boat prompt "quick follow-up" # continues THIS shell's current conversation
boat conversations # list them all, with ids to resume
```
* **`--new`** starts a new conversation and makes it the current conversation of this shell. Each prompt prints its conversation id directly under `queued:` (`conversation: `, with `(new)` when `--new` created it). In `--json`, the id is in `conversationId`.
* **`--resume `** continues a specific conversation. `--resume current` continues the current conversation of this shell.
* **No flag** continues the current conversation of this shell. The "current conversation" belongs to your shell, the same as the `current` sandbox id. Two shells (or two people) can prompt the same sandbox. Each one keeps its own conversation, and they do not interfere.
* **[`boat conversations`](#boat-conversations-id)** lists all conversations on the sandbox with the id, the number of prompts, the running state and the last prompt. You can then resume a conversation from another shell or another day by its id.
* Conversations run in parallel up to a limit for each sandbox. The limit increases with the memory of the sandbox. Above the limit, new prompts wait in a queue and start when turns finish.
* You can continue a conversation with a different `--provider`. The history stays after the change.
For how harnesses, conversations, the agent lifecycle, and parallel runs work together, with diagrams, see [Integrated agents](/integrated-agents).
### `boat ai`
Manage AI credentials: credential profiles, API keys, custom APIs, the AI credentials proxy, usage, and blocking. `--org ` selects an organization for `proxy`, `usage` and `profile new`. See [AI credentials](/ai-credentials).
```bash theme={null}
boat ai accounts chatgpt-subscription # out of credits on this account?
boat ai switch chatgpt-subscription 2 # every sandbox on the profile uses account 2 now
boat ai proxy on
boat ai profiles
boat ai profile new work --copy-from default
boat ai profile default work
echo "$ANTHROPIC_API_KEY" | boat ai profile set-key work anthropic-api-key
echo "$STRIPE_KEY" | boat ai profile set-api work STRIPE_KEY --base-url https://api.stripe.com/v1
boat ai usage bx_23456789 --days 7
boat ai block bx_23456789
```
| Command | Description |
| - | - |
| `boat ai accounts [--profile