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

# Integrated agents

> Boat runs coding-agent harnesses for you: the boilerplate you would otherwise build on a bare cloud computer, with every seam to plug into, customize, or replace.

A Sandbox ships with coding-agent harnesses installed and wired up: **Claude Code, Codex, pi, OpenCode, Prime Agent, and Kimi Code**. `boat prompt` hands one of them a task, streams back what it does, keeps its memory, runs many tasks at once, survives the sandbox stopping and resuming, and lets you switch model or harness mid-task.

None of that is magic. It is exactly what you would build if you rented a bare cloud computer and put an agent on it. This page shows that boilerplate, how Boat implements it, and where you plug in your own instructions, tools, keys, or even your own harness.

## What you'd have to build

Put a coding agent on a plain cloud machine and you always end up writing the same pieces:

| You'd have to build                                                 | Boat does it as                                                                                                                                                          |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Install the harness, keep it updated                                | Six harnesses preinstalled and refreshed on every sandbox                                                                                                                |
| Hand it API keys, or your user's own keys                           | Your keys from the [Agents dashboard](https://boat.dev/dashboard?tab=agents), or per-Sandbox keys with `boat new -e` (see [Whose keys](#whose-keys-yours-or-your-users)) |
| Spawn the harness with a prompt in the right directory              | `boat prompt "…"` / `POST /prompt`, runs in `/home/user`                                                                                                                 |
| Capture stdout, parse tool calls, stream to your UI                 | `boat events` / `GET /events`: structured prompt, response, and tool-call events                                                                                         |
| Remember the session id so the next prompt continues the thread     | A **conversation**. Boat keeps the history and the harness session                                                                                                       |
| Run two tasks at once without their sessions colliding              | Many parallel conversations per sandbox, each with its own harness process                                                                                               |
| Send SIGINT to the right process                                    | `boat interrupt --convo <id>`                                                                                                                                            |
| Type at the agent while it is working, per harness                  | `boat steer "…"` / `POST /steer`, see [Steering a running turn](#steering-a-running-turn)                                                                                |
| Persist sessions to disk so a reboot loses nothing                  | Conversations survive `boat stop`, `boat resume`, `boat fork`                                                                                                            |
| Let the user pick a model, or a thinking level                      | `--model` and `--reasoning-effort` on every prompt                                                                                                                       |
| Let the user pick a harness, and carry the context when they switch | `--provider` on every prompt; the conversation history carries across                                                                                                    |
| Inject house rules, MCP servers, skills, tools                      | Ordinary harness config files in the sandbox home, see [Customize the harness](#customize-the-harness)                                                                   |
| Attach a screenshot or a file                                       | `boat prompt --attach`                                                                                                                                                   |
| Give the agent eyes and hands on a real screen                      | The `computer` tools, on every harness, see [Computer use](#computer-use)                                                                                                |

You can still do any of these by hand. Boat never stops you from `boat ssh`-ing in and running whatever you like (see [Bring your own harness](#bring-your-own-harness)).

## How it fits together

```mermaid theme={null}
flowchart LR
  You["You: CLI, API, SDK"]
  subgraph Backend["Boat backend"]
    Q["Queue prompts<br/>pick harness + model"]
    E["Collect events"]
  end
  subgraph Boat["Your Sandbox"]
    AS["Agent server<br/>(always on)"]
    A["Conversation A<br/>claude process"]
    B["Conversation B<br/>pi process"]
    C["Conversation C<br/>codex process"]
    D[("/home/user<br/>files, config, sessions")]
  end
  You -- "boat prompt" --> Q --> AS
  AS --> A & B & C
  A & B & C --> D
  AS -- "events" --> E -- "boat events" --> You
```

The agent server is a small always-on process on every sandbox. Each **conversation** owns one harness process and one history. The harnesses are ordinary processes running as `user` in `/home/user`, reading ordinary config files and credentials, which is why everything below is inspectable and replaceable.

## Harnesses and models

Pick the harness per prompt with `--provider`, or omit it to use the one you chose on the Agents dashboard.

| `--provider` | Harness                     | Models                                                                            |
| ------------ | --------------------------- | --------------------------------------------------------------------------------- |
| `claude`     | Claude Code                 | Anthropic models, or DeepSeek models on a DeepSeek key                            |
| `codex`      | OpenAI Codex                | OpenAI models, or DeepSeek models on a DeepSeek key                               |
| `pi`         | pi                          | Anthropic, OpenAI, and OpenRouter or llmgateway-routed models (DeepSeek included) |
| `opencode`   | OpenCode                    | same multi-vendor catalog                                                         |
| `prime`      | Prime Agent                 | same multi-vendor catalog                                                         |
| `kimi`       | Kimi Code CLI (Moonshot AI) | Kimi models: K3, Kimi for Coding, K2.7 Code, K2.6                                 |

Every model accepts a different set of reasoning levels, and the catalog changes often, so it is served live rather than written here: `boat prompt --help` prints it, `GET /provider-models` returns it as JSON. Change model or thinking level on any prompt, in the same conversation:

```bash theme={null}
boat prompt --provider pi --model claude-sonnet-5 --reasoning-effort high "Refactor the billing module"
boat prompt --model gpt-5.6-terra "Same task, second opinion"        # same conversation, other model
```

## Whose keys: yours or your users'

Two situations, one mechanism. The harness reads its credentials from the sandbox's environment and auth files, and Boat fills those from one of two sources:

| Source                          | How                                                                                                                          | Who sees the keys                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Your account**                | Connect keys or subscriptions on the [Agents dashboard](https://boat.dev/dashboard?tab=agents) once; every sandbox gets them | You, on your own sandboxes                            |
| **Per Sandbox, from your user** | `boat new --no-env -e ANTHROPIC_API_KEY=… -e OPENAI_API_KEY=…` (API: `noEnv: true` plus `env`)                               | Only that sandbox. Your account keys never land on it |

Subscriptions connect on the Agents dashboard the same way for all three vendors: **Claude** (Pro or Max), **ChatGPT** (Plus, Pro or Team) and **Kimi** (Kimi Code). Click Sign in, approve on the vendor's page (ChatGPT and Kimi show a short code to confirm), and the dashboard reports the subscription as connected. Boat keeps the sign-in alive for you: the token is refreshed server-side before every prompt and every sandbox start, so a sandbox never comes up on an expired one. Sign out on the same row to drop it everywhere.

`--no-env` withholds every credential of yours; the `-e` values are the only ones the sandbox ever has. That is the shape of a product where each end user brings their own key or subscription and picks a harness in a selector:

```mermaid theme={null}
sequenceDiagram
  participant U as Your user
  participant App as Your app
  participant API as Boat API
  U->>App: pastes key, picks Claude Code
  App->>API: POST /sandboxes {noEnv: true, env: {ANTHROPIC_API_KEY}}
  API-->>App: sandbox id
  App->>API: POST /prompt {provider: "claude", prompt}
  API-->>App: events (streamed)
  App-->>U: live output
```

Variables the harnesses read (any subset works; a harness whose vendor key is missing refuses the prompt with a credential error, the others keep working):

| Variable                                            | Used by                                                                                                                                                    |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ANTHROPIC_API_KEY`                                 | Claude Code, pi, OpenCode, Prime Agent                                                                                                                     |
| `OPENAI_API_KEY`                                    | Codex, pi, OpenCode, Prime Agent                                                                                                                           |
| `OPENROUTER_API_KEY`                                | pi, OpenCode, Prime Agent                                                                                                                                  |
| `LLMGATEWAY_API_KEY`                                | pi, OpenCode, Prime Agent                                                                                                                                  |
| `CLAUDE_CODE_OAUTH_TOKEN`                           | Claude Code, a Claude Pro or Max subscription token from `claude setup-token`                                                                              |
| `MOONSHOT_API_KEY`                                  | Kimi Code, a Kimi open platform key (api.moonshot.ai)                                                                                                      |
| `KIMI_CODE_ACCESS_TOKEN`, `KIMI_CODE_REFRESH_TOKEN` | Kimi Code, a Kimi Code subscription token pair (the `access_token` and `refresh_token` a `kimi login` stores in `~/.kimi-code/credentials/kimi-code.json`) |
| `DEEPSEEK_API_KEY`                                  | Claude Code and Codex, a [DeepSeek platform key](https://platform.deepseek.com/api_keys), see [DeepSeek](#deepseek)                                        |
| `AWS_BEARER_TOKEN_BEDROCK` + `AWS_REGION`           | Claude Code (with `CLAUDE_CODE_USE_BEDROCK=1`) and Codex (with `OPENAI_BASE_URL`), see [Amazon Bedrock](#amazon-bedrock)                                   |

Kimi Code reads no credential from the environment itself; the sandbox writes these into its `~/.kimi-code/config.toml` and token file before the first prompt, then the CLI refreshes the subscription token on its own.

Keys are per sandbox, not per conversation. A Sandbox shared by several users runs on one set of credentials; give each paying user their own sandbox when their keys must stay apart, and share one sandbox across conversations when the keys are yours.

### DeepSeek

Claude Code and Codex can run on DeepSeek's models instead of Anthropic's or OpenAI's. One credential covers both: a [DeepSeek platform key](https://platform.deepseek.com/api_keys), a single string starting with `sk-`. DeepSeek publishes an Anthropic-shaped endpoint and an OpenAI-shaped one against the same key, so neither harness is modified and neither needs a gateway in front of it.

| Source                          | How                                                                                                                                                                         |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Your account**                | Agents dashboard, the DeepSeek API key row: paste the key once, then point Claude Code or Codex (or both) at it and pick a DeepSeek model                                   |
| **Per Sandbox, from your user** | `boat new --no-env -e DEEPSEEK_API_KEY=sk-…`. That one variable configures both harnesses; pass another vendor's key alongside it and that harness keeps its own credential |

What the sandbox exports to each harness, and which model ids to pass:

| Harness     | Environment                                                                                                                                                                                                                                          | Model ids                                                                                                                                                    |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Claude Code | `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic`, `ANTHROPIC_AUTH_TOKEN` = the key, `ANTHROPIC_API_KEY` empty (Claude Code otherwise sends an `x-api-key` header the endpoint rejects)                                                        | `deepseek-flash`, `deepseek-v4-pro`. DeepSeek also maps Claude's own names onto these, so the harness's small, fast and subagent models resolve on their own |
| Codex       | `OPENAI_BASE_URL=https://api.deepseek.com/v1` and the key; the agent server writes a `model_providers.deepseek` block pointing at `https://api.deepseek.com/` with `wire_api = "responses"` (Codex ignores `OPENAI_BASE_URL` itself), web search off | `deepseek-flash`, `deepseek-v4-pro`. Reasoning levels are `low` and `high`, the two both Codex and DeepSeek accept                                           |

<CodeGroup>
  ```bash CLI theme={null}
  boat new --no-env -e DEEPSEEK_API_KEY=sk-…
  boat prompt --provider claude --model deepseek-flash "Summarize this repo"
  boat prompt --provider codex --model deepseek-v4-pro --reasoning-effort high "Now review the diff"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"noEnv":true,"env":{"DEEPSEEK_API_KEY":"sk-…"}}'
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/prompt" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"provider":"claude","model":"deepseek-flash","prompt":"Summarize this repo"}'
  ```

  ```ts TypeScript theme={null}
  const created = await sandbox.create({
    createSandboxRequest: { noEnv: true, env: { DEEPSEEK_API_KEY: "sk-…" } },
  });
  await sandbox.prompt({
    sandboxId: created.sandbox.id,
    promptRequest: { provider: "claude", model: "deepseek-flash", prompt: "Summarize this repo" },
  });
  ```

  ```python Python theme={null}
  created = sandbox.create(CreateSandboxRequest(no_env=True, env={"DEEPSEEK_API_KEY": "sk-…"}))
  sandbox.prompt(created.sandbox.id, PromptRequest(provider="claude", model="deepseek-flash", prompt="Summarize this repo"))
  ```
</CodeGroup>

pi, OpenCode and Prime Agent reach DeepSeek a different way: through your OpenRouter key, with no DeepSeek account at all. Those models are `openrouter:deepseek/deepseek-v4.1-flash`, `openrouter:deepseek/deepseek-v4-flash-0731` and `openrouter:deepseek/deepseek-v4-pro-0813`, and they need only `OPENROUTER_API_KEY`.

### Amazon Bedrock

Claude Code and Codex can run on models served by your AWS account instead of Anthropic's or OpenAI's API. The simplest credential is a [Bedrock API key](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html): one string starting with `ABSK`, generated in the Bedrock console under API keys, valid in every region. IAM access keys (access key id, secret, optional session token) work too.

| Source                          | How                                                                                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Your account**                | Agents dashboard, the Bedrock row under Claude Code or Codex: region, then API key or IAM keys. Point the harness at it and pick a Bedrock model |
| **Per Sandbox, from your user** | `boat new --no-env -e AWS_BEARER_TOKEN_BEDROCK=ABSK… -e AWS_REGION=us-east-1 -e CLAUDE_CODE_USE_BEDROCK=1`                                       |

What the sandbox exports to each harness, and which model ids to pass:

| Harness     | Environment                                                                                                                                                                                                                                                                                                                                       | Model ids                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code | `CLAUDE_CODE_USE_BEDROCK=1`, `AWS_REGION`, the key (or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)                                                                                                                                                                                                                                             | Bedrock cross-region inference profiles on the Anthropic line: `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-6-v1`, `us.anthropic.claude-sonnet-4-5-20250929-v1:0`, `us.anthropic.claude-opus-4-5-20251101-v1:0`, `us.anthropic.claude-haiku-4-5-20251001-v1:0`, `us.anthropic.claude-sonnet-5`, `us.anthropic.claude-opus-5`, `us.anthropic.claude-fable-5-1`, `us.anthropic.claude-fable-5`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7`. With the Bedrock credential in use these are the only Claude Code models offered, and the default becomes Sonnet 4.6: the plain `sonnet` and `opus` aliases resolve inside Claude Code to model ids many AWS accounts have not enabled |
| Codex       | `OPENAI_BASE_URL=https://bedrock-mantle.<region>.api.aws/v1` and the key; the agent server selects Codex's built-in `amazon-bedrock` provider for that region (Codex ignores `OPENAI_BASE_URL` itself) on Bedrock's OpenAI-compatible [Mantle endpoint](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html), web search off | The OpenAI line on Mantle: `openai.gpt-5.6-terra`, `openai.gpt-5.6-sol`, `openai.gpt-5.6-luna`, `openai.gpt-5.5`, `openai.gpt-5.5-2026-04-23`, `openai.gpt-5.4`, `openai.gpt-5.4-2026-03-05`. Codex on Bedrock needs the API key; IAM keys alone do not authenticate the OpenAI-compatible endpoint. The open-weight `gpt-oss` models and the third-party models on Mantle (Kimi, MiniMax, Qwen, DeepSeek, GLM, Mistral) are not listed: they answer Mantle's `/v1` routes but not the `/openai/v1/responses` route Codex calls                                                                                                                                                                                       |

<CodeGroup>
  ```bash CLI theme={null}
  boat new --no-env -e AWS_BEARER_TOKEN_BEDROCK=ABSK… -e AWS_REGION=us-east-1 -e CLAUDE_CODE_USE_BEDROCK=1
  boat prompt --provider claude --model us.anthropic.claude-sonnet-4-6 "Summarize this repo"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"noEnv":true,"env":{"AWS_BEARER_TOKEN_BEDROCK":"ABSK…","AWS_REGION":"us-east-1","CLAUDE_CODE_USE_BEDROCK":"1"}}'
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/$BOAT_ID/prompt" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"provider":"claude","model":"us.anthropic.claude-sonnet-4-6","prompt":"Summarize this repo"}'
  ```

  ```ts TypeScript theme={null}
  const created = await sandbox.create({
    createSandboxRequest: { noEnv: true, env: { AWS_BEARER_TOKEN_BEDROCK: "ABSK…", AWS_REGION: "us-east-1", CLAUDE_CODE_USE_BEDROCK: "1" } },
  });
  await sandbox.prompt({
    sandboxId: created.sandbox.id,
    promptRequest: { provider: "claude", model: "us.anthropic.claude-sonnet-4-6", prompt: "Summarize this repo" },
  });
  ```

  ```python Python theme={null}
  created = sandbox.create(CreateSandboxRequest(no_env=True, env={"AWS_BEARER_TOKEN_BEDROCK": "ABSK…", "AWS_REGION": "us-east-1", "CLAUDE_CODE_USE_BEDROCK": "1"}))
  sandbox.prompt(created.sandbox.id, PromptRequest(provider="claude", model="us.anthropic.claude-sonnet-4-6", prompt="Summarize this repo"))
  ```
</CodeGroup>

#### Model access is granted per AWS account and region

The catalog above lists what Amazon Bedrock offers for each harness, not what your account may call today. Bedrock grants model access per AWS account and per region, so a model is listed here as soon as Bedrock serves it, including frontier models AWS has not granted you yet. Whether you can call one is between your account and AWS.

To request access: open the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/) in the region your credential uses, go to Model catalog, find the model, and choose Request model access. Some models ask for a short use case form and are approved by AWS rather than instantly. Access granted in one region does not carry to another.

Pick a model your account has not been granted and the prompt comes back naming the model, the reason and the fix, for example:

```
Amazon Bedrock has not granted this AWS account access to anthropic.claude-opus-5. Bedrock grants
model access per AWS account and per region, so a model in the sandbox catalog still has to be enabled
on your side: open the Amazon Bedrock console in the region this sandbox's credential uses, find
anthropic.claude-opus-5 in the Model catalog, request access, and retry once AWS approves it. Pick a
model you already have access to in the meantime.
```

Two related failures read differently. A key AWS rejects outright surfaces as an authentication error, with the harness naming the endpoint it called. A model id whose region prefix does not match your credential's region comes back as `The provided model identifier is invalid`: the `us.` profiles above only resolve from US regions, so from `eu-west-1` pass `eu.anthropic.claude-sonnet-4-6` instead, and likewise `au.` or `jp.`. A `global.` prefix works from any supported source region and needs a wider IAM policy, see [global cross-region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/global-cross-region-inference.html).

## Conversations: the harness's memory

The first `boat prompt` on a sandbox starts a **conversation**: a thread of prompts and responses with its own memory, backed by the harness's native session. Every later prompt continues it. You never handle the session id, but it is there when you want it: every prompt prints its conversation id (`conversation: <id>` under `queued:`, `conversationId` in `--json` and in the API response) and every event carries one.

```bash theme={null}
boat prompt "Now add tests"                              # this shell's current conversation
boat prompt --new "Look into the flaky CI job"           # a brand-new conversation
boat prompt --resume <id> "Continue where we left off"   # a specific one
boat conversations                                       # every conversation on the sandbox, with ids
```

`boat conversations` (`GET /sandboxes/{id}/conversations`) lists them newest first with prompt count, whether a turn is running, the last harness and model, a preview of the last prompt, and which one is the sandbox's current conversation. That is how a thread started yesterday, or from another shell, gets its id back for `--resume`.

**Current is per shell.** Like the `current` Sandbox id, the current conversation is scoped to the shell you are in: `--new` makes the new conversation this shell's current one, and a bare `boat prompt` continues it. Two shells, two people, or two machines prompting the same sandbox each keep their own thread and never trip over each other.

## Parallel conversations

Conversations run at the same time, each in its own harness process, with isolated histories:

```mermaid theme={null}
gantt
  dateFormat  s
  axisFormat  %S s
  section A claude
  refactor billing        :a1, 0, 40
  add tests (waits for A) :a2, after a1, 25
  section B pi
  fix flaky test          :b1, 0, 30
  section C codex
  write the changelog     :c1, 5, 20
```

The rules are the ones you would write yourself:

* **One turn at a time per conversation.** A second prompt to the same conversation waits for the current turn, because it needs that turn's context. Prompts to different conversations run concurrently.
* **A per-Sandbox cap** on concurrent turns, sized to the sandbox's memory: a `default` (8 GB) Boat runs roughly two dozen at once, `large` more, `small` fewer. Past the cap, prompts queue and start as turns finish; nothing is dropped. Pin an exact number with the `ASCII_MAX_PARALLEL_CONVERSATIONS` environment variable on the sandbox, or size the sandbox ([Machine Capabilities](/machines)).
* **List them** with `boat conversations`: each row says whether a turn is running in it, so you can see the parallel work at a glance.
* **Events stream all conversations by default**, tagged with `conversationId`. Watch one with `boat events --convo <id>`.
* **Interrupt is scoped.** `boat interrupt --convo <id>` stops one turn; the others keep running. Bare `boat interrupt` stops everything on the sandbox.
* **Steer is scoped too.** `boat steer --convo <id> "..."` changes one running turn without stopping it. See [Steering a running turn](#steering-a-running-turn).

## Steering a running turn

A second `boat prompt` waits for the running turn. `boat interrupt` throws it away. **`boat steer` is the third option**: it hands the running turn a new message, the agent takes it into account, and it keeps everything it was doing. This is what typing into a coding agent while it works does.

```bash theme={null}
boat prompt "Refactor billing and run the full test suite"
boat steer "Skip the integration tests, the unit tests are enough"
boat steer --convo <id> "Also update the changelog when you are done"
```

Omit `--convo` and it steers the conversation this shell last prompted, exactly like a bare `boat prompt` continues it.

```mermaid theme={null}
sequenceDiagram
  participant You
  participant Turn as The running turn
  You->>Turn: boat prompt "refactor billing, run the tests"
  Note over Turn: working, tests running
  You->>Turn: boat steer "skip the integration tests"
  Note over Turn: still the same turn, now with your instruction
  Turn-->>You: finishes the whole thing
```

**How it behaves per harness.** Four of the six take a mid-turn message natively, so nothing is stopped. OpenCode and Kimi Code have no such primitive (their protocol has "prompt" and "cancel", nothing in between), so Boat does the next best thing transparently:

| Harness     | Delivery                                                                                                                                                       | Turn interrupted?               |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| Claude Code | Native. Delivered inside the turn when the harness folds it in; otherwise continued right after, as its own turn on the same session.                          | No                              |
| Codex       | Native. `turn/steer` adds your message to the in-flight turn.                                                                                                  | No                              |
| pi          | Native. Delivered at the turn's next model call, which is usually right after the tool in flight finishes.                                                     | No                              |
| Prime Agent | Native. Same as pi.                                                                                                                                            | No                              |
| OpenCode    | Fallback. Sandbox stops the turn and immediately starts a new one on the **same** conversation, carrying "Continue what you were doing" plus your instruction. | Yes, and it continues by itself |
| Kimi Code   | Fallback. Same as OpenCode.                                                                                                                                    | Yes, and it continues by itself |

**A steer is always acted on, never just accepted.** Handing a message to a harness is not the same as the harness doing something about it, so Boat does not take the harness's word for it. It watches the harness's own steering queue where there is one, waits for the queue to drain before it calls the turn finished, and where it cannot see delivery it runs the instruction as its own turn the moment the turn ends. So when `boat steer` returns and the turn later reports `finished`, the instruction has been carried out, not merely delivered.

The API response and the event both carry `native`, and the event carries the exact `mode`: `native`, `native-continued`, `fallback`, or `late`. **`native-continued`** is the honest middle case: the harness accepted the message but its turn ended without acting on it, so Boat ran it immediately as its own turn on the same session. Nothing was interrupted and nothing was forgotten, the instruction simply lands a turn boundary later.

The `fallback` path keeps the harness session and all of its memory too, so nothing is forgotten there either; what it costs is the one tool call that was in flight, and the agent may redo a little of what it had just started.

A steer is its own event in the stream, never a queued prompt:

```bash theme={null}
boat events --convo <id>
```

```
you [prompt]: Refactor billing and run the full test suite
you [steer]: Skip the integration tests, the unit tests are enough
...
```

Steering a conversation with nothing in flight is refused with `no_running_turn` (409). That is deliberate: you asked to change a turn that is running, and quietly queueing a new one instead would be a surprise. Use `boat prompt` for that.

## Agent lifecycle

A prompt moves through a small state machine you can observe with `boat events` or `GET /prompts/{promptId}`:

```mermaid theme={null}
stateDiagram-v2
  [*] --> queued: boat prompt
  queued --> running: conversation free and Boat under its cap
  running --> running: boat steer (message joins this turn)
  running --> finished
  running --> interrupted: boat interrupt
  running --> failed: harness or credential error (reason in the event)
  finished --> [*]
  interrupted --> [*]
  failed --> [*]
```

A steer never creates a state of its own: the turn that was already running is still the turn that finishes, and the work your message causes is part of it.

The Sandbox lifecycle sits underneath, and conversations ride along with it:

```mermaid theme={null}
flowchart LR
  N["boat new"] --> R["ready"]
  R --> P["prompt … prompt"]
  P --> S["boat stop<br/>disk snapshot incl. sessions"]
  S --> R2["boat resume / boat fork"]
  R2 --> P2["boat prompt --resume id<br/>same memory"]
```

Stopping snapshots the disk, including every conversation's history and the harness's native session files under `/home/user`. A resumed or forked sandbox restores them, so `--resume <id>` picks up with full memory. Processes the harness started by hand (a dev server it launched, a tunnel) do not survive a stop, same as on any reboot; conversations and every config file below do.

## Switching harness or model mid-conversation

Continue a conversation on a different harness and it keeps the thread:

```mermaid theme={null}
sequenceDiagram
  participant You
  participant Conv as Conversation A
  participant CC as Claude Code
  participant Pi as pi
  You->>Conv: prompt 1, prompt 2 (--provider claude)
  Conv->>CC: turns
  You->>Conv: --resume A --provider pi "take it from here"
  Conv->>Pi: full earlier transcript + new prompt
  Pi-->>You: continues with the whole context
```

```bash theme={null}
boat prompt --resume <A> --provider pi --model claude-sonnet-5 "Take it from here"
```

Boat freezes the earlier harness's transcript into the conversation and hands it to the new one, so the switch is one flag rather than a context-rebuilding script. A user-facing selector for harness and model maps directly onto `provider` and `model` on `POST /prompt`.

## Computer use

Every Sandbox runs a Linux desktop, and every harness on it can see and drive that desktop as ordinary tools. Nothing to install, nothing to register:

```bash theme={null}
boat prompt "Open example.com in the browser and tell me the page title"
boat prompt --provider codex "Log into the admin app in Chrome and screenshot the dashboard"
```

The tools come from the [Cua Driver](https://github.com/trycua/cua), preinstalled on every sandbox and registered as an MCP server named `computer` for all six harnesses:

```mermaid theme={null}
flowchart LR
  P["boat prompt"] --> H["Harness process<br/>claude / codex / pi / opencode / prime / kimi"]
  H -- "stdio MCP<br/>server name: computer" --> M["cua-driver mcp"]
  M -- "unix socket" --> D["cua-driver daemon<br/>systemd user service"]
  D --> X["The Sandbox desktop<br/>Xorg :0, 1920x1080, Chrome"]
  X -.-> S["boat desktop<br/>watch it live"]
```

The daemon runs as the desktop user, so the tools act on the very screen `boat desktop` streams: open a stream in one window and watch the agent work in it.

### What the agent gets

| Group            | Tools                                                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Look             | `get_window_state` (screenshot plus the accessibility tree in one call), `get_desktop_state`, `get_screen_size`, `list_windows`, `list_apps` |
| Point and type   | `click`, `double_click`, `right_click`, `drag`, `scroll`, `move_cursor`, `type_text`, `press_key`, `hotkey`, `set_value`                     |
| Apps and windows | `launch_app`, `kill_app`, `bring_to_front`, `set_window_frame`, `invoke_menu`                                                                |
| Browser          | `browser_navigate`, `browser_click`, `browser_type`, `get_browser_state`, `browser_download`                                                 |
| Clipboard        | `clipboard_read`, `clipboard_write`                                                                                                          |

Clicks and keystrokes are delivered in the background by default: they land on the target window without raising it or moving the real pointer, so several windows can be driven without a focus fight. When a widget only accepts input while focused, the tool says so and the agent can retry with `delivery_mode: "foreground"`, which activates the window, acts, and puts focus back.

Tool names reach `boat events` namespaced by the harness, for example `mcp__computer__click` on Claude Code and Kimi Code, and `mcp:computer/click` on Codex.

### Where it is registered

The registration is an ordinary entry in each harness's own config file, written for you at sandbox start, and merged with anything you put there yourself:

| Harness     | File                               | Entry                                       |
| ----------- | ---------------------------------- | ------------------------------------------- |
| Claude Code | `~/.claude.json`                   | `mcpServers.computer`                       |
| Codex       | `~/.codex/config.toml`             | `[mcp_servers.computer]`, in a marked block |
| pi          | `~/.pi/agent/mcp.json`             | `mcpServers.computer`                       |
| OpenCode    | `~/.config/opencode/opencode.json` | `mcp.computer`                              |
| Prime Agent | `~/.prime/agent/settings.json`     | `mcpServers.computer`                       |
| Kimi Code   | `~/.kimi-code/mcp.json`            | `mcpServers.computer`                       |

Your own MCP servers in those files are untouched. Remove `computer` from a file and that harness simply loses the tools; it is rewritten on the next sandbox start, so delete the sandbox's daemon if you want it gone for good.

One tool, `browser_prepare`, is switched off for OpenCode in `opencode.json` `tools`, because OpenCode forwards MCP tool schemas to the model provider unchanged and the Anthropic API rejects that tool's schema, which would otherwise fail every OpenCode turn on an Anthropic model. Prime Agent reaches the server from inside its Python tool, as `await mcp.call_tool("computer", "click", {...})`.

### Practical notes

* **One screen, shared.** All conversations on a sandbox drive the same desktop. Two agents clicking at once will fight over it. Give each parallel GUI task its own sandbox, or serialize them.
* **It survives stop, resume and fork.** The daemon is a systemd user service on the sandbox, brought back automatically. The socket lives on `/run`, so a resumed sandbox never inherits a stale one.
* **Logins persist.** A Chrome profile the agent signed into is part of the sandbox's disk, so it comes back on resume and travels to a fork.
* **Screenshots are large.** A full-desktop screenshot is a real image in the model's context. Ask for a specific window (`get_window_state` with a window id) when a task loops.
* **Watch it, and record it.** Open the stream with `boat desktop` to see the agent work, and capture the run to an MP4 with `ascii-record-desktop`. See [Desktop Streaming](/desktop-streaming).

If a prompt says it has no computer tools, check the daemon:

```bash theme={null}
boat exec 'systemctl --user status cua-driver.service --no-pager'
boat exec '/opt/ascii/cua-driver/cua-driver status --socket /run/ascii-cua/driver.sock'
boat exec 'systemctl --user restart cua-driver.service'
```

## Customize the harness

Every harness reads its own config files from the sandbox home. Boat adds nothing on top except its own short system prompt (which tells the harness it is running headless in a sandbox and that the `boat` CLI exists) and the `boat` skill. Everything else is yours to add, over `boat ssh`, `boat exec`, `boat scp`, a file the agent writes, an [environment setup script](/environments), or a [named snapshot](/snapshots) so every new Sandbox starts with it.

All of it was verified end to end through `boat prompt` on the current harness versions; each row names the file that made the harness change its answer.

### Instructions and hidden rules

Drop a rules file and every prompt on that sandbox obeys it, without the prompt mentioning it:

| Harness     | Files read (all applied together)                                                             | Not read                                                                 |
| ----------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Claude Code | `~/CLAUDE.md`, `~/.claude/CLAUDE.md`                                                          | `AGENTS.md`                                                              |
| Codex       | `~/AGENTS.md`, `~/.codex/AGENTS.md`                                                           | `config.toml` `developer_instructions` (Boat sets the base instructions) |
| pi          | `~/AGENTS.md`, `~/.pi/agent/AGENTS.md`, `~/.pi/agent/APPEND_SYSTEM.md`                        | `CLAUDE.md` when an `AGENTS.md` exists                                   |
| OpenCode    | `~/AGENTS.md`, `~/.config/opencode/AGENTS.md`, files listed in `opencode.json` `instructions` |                                                                          |
| Prime Agent | `~/AGENTS.md`, `~/.prime/agent/AGENTS.md`, `~/.prime/agent/APPEND_SYSTEM.md`                  |                                                                          |
| Kimi Code   | `~/AGENTS.md`, `~/.kimi-code/AGENTS.md`, `~/.agents/AGENTS.md`                                | `CLAUDE.md`                                                              |

One `~/AGENTS.md` plus one `~/CLAUDE.md` therefore covers all six. `APPEND_SYSTEM.md` (pi, Prime) is appended to the system prompt itself rather than to the project context. When an environment clones a single repository, Claude Code starts inside that repository, so a `CLAUDE.md` there applies too.

```bash theme={null}
boat exec "printf '# House rules\nAlways run the test suite before saying you are done.\n' > ~/AGENTS.md; cp ~/AGENTS.md ~/CLAUDE.md"
```

### MCP servers

| Harness     | How to register                                                | Config written                                                                           |
| ----------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Claude Code | `claude mcp add --scope user <name> -- <command>`              | `~/.claude.json` (project scope: `~/.mcp.json`)                                          |
| Codex       | `codex mcp add <name> -- <command>`                            | `~/.codex/config.toml` `[mcp_servers.<name>]`                                            |
| pi          | `pi install npm:pi-mcp-extension`, then `~/.pi/agent/mcp.json` | `{"mcpServers": {"<name>": {"transport": "stdio", "command": …, "lifecycle": "eager"}}}` |
| OpenCode    | `~/.config/opencode/opencode.json`                             | `"mcp": {"<name>": {"type": "local", "command": [...]}}`                                 |
| Prime Agent | `prime-agent mcp add <name> -- <command>`                      | `~/.prime/agent/settings.json` `mcpServers`                                              |
| Kimi Code   | `~/.kimi-code/mcp.json`                                        | `{"mcpServers": {"<name>": {"command": …, "args": [...]}}}` (`{"url": …}` for HTTP)      |

Remote (HTTP) servers work the same way with a URL instead of a command. Claude Code's MCP resource browsing tools are disabled on a sandbox; MCP tools are not.

Every Sandbox already has one server registered this way, `computer`, in exactly these files. Adding yours next to it changes nothing about it, and it never overwrites yours. See [Computer use](#computer-use).

### Reach your own app from the sandbox

A Sandbox has no route back to your laptop, so an MCP server, a local model, or a webhook receiver running on `localhost` is invisible to the agents inside it. `boat forward --reverse` opens that route: a port on your machine starts answering at `127.0.0.1:<port>` inside the sandbox, over the same SSH session `boat ssh` uses, with nothing exposed publicly on either end.

```bash theme={null}
# leave this running; your MCP server on localhost:7777 is now in the sandbox
boat forward bx_f7k2q9hd --reverse --local 7777
```

Register it once inside the sandbox and every harness can call it:

```bash theme={null}
boat exec bx_f7k2q9hd "claude mcp add --scope user mine --transport http http://127.0.0.1:7777/mcp"
```

The registration lives in the sandbox home, so it survives `boat stop`, `boat resume`, and `boat fork`; the tunnel does not, and has to be started again next time you want the agent to reach you. A local model works the same way: `boat forward <id> --reverse --local 11434` puts Ollama on `http://127.0.0.1:11434` inside the sandbox, ready for any harness pointed at that base URL. See [`boat forward --reverse`](/cli-reference#--reverse-reach-your-machine-from-the-sandbox) for the flags and the redial behaviour.

Products that do not ship the CLI get the same tunnel in three lines, because the CLI is only wrapping stock OpenSSH:

```bash theme={null}
# 1. authorize your public key on the sandbox; the reply carries machineIp
curl -s -X POST https://boat.dev/api/v1/sandboxes/$BOAT_ID/sshkey \
  -H "Authorization: Bearer $BOAT_API_KEY" -H 'Content-Type: application/json' \
  -d "{\"key\": \"$(cat ~/.ssh/id_ed25519.pub)\"}"
# 2. open the reverse tunnel and leave it running
ssh -o ExitOnForwardFailure=yes -i ~/.ssh/id_ed25519 -N -R 7777:127.0.0.1:7777 user@<machineIp>
```

### Skills

A skill is a folder with a `SKILL.md` (frontmatter `name` and `description`, then the instructions). Every harness on a sandbox already has a skills directory with the `boat` skill in it; add yours next to it:

| Harness     | Skills directory                                                                |
| ----------- | ------------------------------------------------------------------------------- |
| Claude Code | `~/.claude/skills/<name>/SKILL.md`                                              |
| Codex       | `~/.codex/skills/<name>/SKILL.md`                                               |
| pi          | `~/.pi/agent/skills/<name>/SKILL.md`                                            |
| OpenCode    | `~/.config/opencode/skills/<name>/SKILL.md`                                     |
| Prime Agent | `~/.prime/agent/skills/<name>/SKILL.md`                                         |
| Kimi Code   | `~/.kimi-code/skills/<name>/SKILL.md` (also `~/.agents/skills/<name>/SKILL.md`) |

### Command-line tools

Anything on `PATH` is a tool. Put a script in `~/.local/bin` or `/usr/local/bin` (or `npm i -g`, `pip install`, `apt install` it) and ask for it by name; all six harnesses run it through their shell tool.

### Custom in-process tools and extensions

For a tool that should show up as a native function call rather than a shell command:

| Harness     | Mechanism                                                                                                      | Path                                                                                           |
| ----------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| pi          | extension calling `pi.registerTool({...})`                                                                     | `~/.pi/agent/extensions/<name>.ts`                                                             |
| Prime Agent | same extension API                                                                                             | `~/.prime/agent/extensions/<name>.ts`                                                          |
| OpenCode    | `tool({description, args, execute})` from `@opencode-ai/plugin` (dependencies install themselves on first run) | `~/.config/opencode/tools/<name>.ts`                                                           |
| Claude Code | hooks in `~/.claude/settings.json`, plugins                                                                    | see [Claude Code docs](https://code.claude.com/docs/en/hooks)                                  |
| Codex       | hooks in `~/.codex/hooks.json`                                                                                 | see [Codex docs](https://learn.chatgpt.com/docs/hooks)                                         |
| Kimi Code   | `[[hooks]]` in `~/.kimi-code/config.toml`, plugins                                                             | see [Kimi Code docs](https://www.kimi.com/code/docs/en/kimi-code-cli/customization/hooks.html) |

### What is shared, what persists

* **Config is per sandbox, not per conversation.** Every parallel conversation on a sandbox, whatever its harness, reads the same home directory: one `AGENTS.md`, one skills folder, one MCP list. Per-user rules need per-user sandboxes, or rules stated in the prompt.
* **Everything in `/home/user` is captured on stop.** Rules, MCP registrations, skills, extensions, tools you installed under home, and the harness sessions all come back on `boat resume` and `boat fork`. Tools installed outside home (`apt`, `/usr/local`) are part of the system snapshot too.
* **Bake it once.** An [environment](/environments) setup script or a [named snapshot](/snapshots) gives every new Sandbox the same rules, tools, and servers from the first prompt.

## Bring your own harness

Boat's built-in harnesses are ordinary binaries on `PATH` with the same credentials the agent server uses. Three ways to go beyond `boat prompt`:

* **Drive a built-in harness yourself.** `boat ssh` or `boat exec` and run `claude`, `codex`, `pi`, `opencode`, `prime-agent`, or `kimi` directly, in any mode they support. The agent server does not lock the files or the processes.
* **Install a harness Boat does not ship.** `boat exec "npm i -g <harness>"` (or bake it into an environment or snapshot) and drive it over `boat exec` or SSH. It coexists with the built-in ones and reads the same per-Sandbox `-e` keys.
* **Run your own agent loop.** Put a small HTTP daemon in the sandbox and talk to it directly: the [Platform Guide](/platform-guide#bring-your-own-harness-the-daemon-pattern) walks through it. `boat host` gives it a URL, [Webhooks](/webhooks) tell your control plane when the sandbox is up.

Whatever you run, `boat events`, attachments under `~/attachments`, [desktop streaming](/desktop-streaming), snapshots, and forks keep working around it.

## Reference

* CLI flags: [`boat prompt`](/cli-reference#boat-prompt-id), [`boat conversations`](/cli-reference#boat-conversations-id), [`boat steer`](/cli-reference#boat-steer-id-message), `boat events --convo`, `boat interrupt --convo`
* API: [Prompt](/api/reference/agent/prompt-sandbox-agent), [Conversations](/api/reference/agent/list-sandbox-conversations), [Events](/api/reference/agent/list-sandbox-events), [Steer](/api/reference/agent/steer-a-running-turn), [Interrupt](/api/reference/agent/interrupt-running-agent-work), with the same `new`, `conversationId`, and `conversation` fields
* SDKs: [Python](/sdks/python), [TypeScript](/sdks/typescript)
* Building a product on top: [Use in code](/use-in-code), [Platform Guide](/platform-guide)
