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

# Environments

> Decide what every new Sandbox starts with: repositories, secrets, and which of your credentials it may use.

An **environment** is the template a new Sandbox inherits when it starts: which GitHub repositories are cloned, which secrets are injected, and which of your credentials the sandbox may use. Manage it from [Dashboard > Environment](https://boat.dev/dashboard?tab=environment) or from the `boat env` commands.

Every account has one environment named `base`. You can add more, and name the one a sandbox should use when you create, resume, or fork it.

<Note>
  This page is about what goes **into** a sandbox. For running code inside one, see [Setup & Scripts](/setup).
</Note>

## Safe for third parties

One switch decides the entire security posture of an environment.

<CardGroup cols={2}>
  <Card title="On" icon="shield-check">
    For Sandboxes other people drive, such as your own end users. **Nothing of yours is passed**: no GitHub access, no secrets, no sandbox or Agents credentials, whatever the section toggles say. The Sandbox is confined to itself and cannot act on your account or your other sandboxes.
  </Card>

  <Card title="Off" icon="user">
    For Sandboxes only you drive. The four section toggles below apply, so you choose exactly what goes in.
  </Card>
</CardGroup>

<CodeGroup>
  ```bash CLI theme={null}
  boat env set base --safe-for-third-parties true
  ```

  ```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}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.updateEnvironment({
    environmentId: envId,
    updateSandboxEnvironmentRequest: { safeForThirdParties: true },
  });
  ```

  ```python Python theme={null}
  sandbox.update_environment(env_id, UpdateSandboxEnvironmentRequest(safe_for_third_parties=True))
  ```
</CodeGroup>

The `--no-env` flag (`noEnv` in the API) is the per-Sandbox shortcut for the same guarantee and is kept forever. `boat new --no-env` behaves exactly like starting in an environment marked safe for third parties. Prefer the environment when more than the occasional sandbox needs it: it holds for every sandbox that uses it, and it survives forks and resumes without your code passing a flag.

### What a normal Boat receives

To know what this protects you from, here is what a sandbox gets from your account when nothing is withheld.

|                       | Passed in                                                                                                                                                                                                                                                                                                             |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Environment variables | Your environment's variables, your GitHub token, and your model credentials if you configured agents (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `CHATGPT_ACCOUNT_ID`), plus neutral Boat-internal vars (`BOAT_ID`, a machine-scoped `ASCII_TOKEN`)                                            |
| Credential files      | Your secret files, the GitHub CLI login (`~/.config/gh/hosts.yml`), git credentials in cloned repos, Claude and Codex logins (`~/.claude/.credentials.json`, `~/.codex/auth.json`), the in-sandbox Boat CLI token (`~/.config/ascii/boat/config.json`), and the shell environment the sandbox exports to SSH sessions |

A protected Boat receives none of that. It keeps only the neutral Boat-internal vars and whatever you pass with `env`.

### Converting an existing sandbox

`boat resume <id> --no-env` and `boat fork <id> --no-env` convert a sandbox whose snapshot came from a normal one. Before the sandbox becomes reachable, every owner secret the snapshot may carry is scrubbed: the managed `~/.bashrc` blocks, `~/.config/gh/hosts.yml` (plus a `gh` logout), `~/.git-credentials`, `~/.ssh/id_*` private keys, the Codex and Claude credential files, the in-sandbox Boat CLI token, and every secret file you configured. `authorized_keys` and `known_hosts` are kept so the sandbox stays reachable.

Credentials the platform never wrote are left alone: `aws`, `gcloud`, `.netrc`, `.npmrc`, and Docker logins added inside the sandbox all stay.

While the scrub runs, SSH and desktop return a retryable `boat_securing` error. Conversion is one way: the sandbox stays protected afterwards.

<Warning>
  The Claude and Codex credential files are removed even when the sandbox's own user logged in with their personal account inside the sandbox. The scrub cannot tell whose they are. Back them up and restore them afterwards if they belong to the sandbox's user.
</Warning>

## What a sandbox can be given

With *Safe for third parties* off, four independent toggles decide what a sandbox receives. Each is a section in the dashboard; open a section to edit what is inside it.

| Section             | What the sandbox gets                                                      | Turning it off                                                  |
| ------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- |
| GitHub repositories | Your repositories cloned in, plus the GitHub token                         | `gh` and `git push` stop working                                |
| Secrets             | Your environment variables and secret files                                | Sandboxes drop them at their next start. What you typed is kept |
| Boat credentials    | A scoped Boat API key, so the sandbox can snapshot, stop and resume itself | The Sandbox cannot manage its own lifecycle                     |
| Agents credentials  | The provider keys (Claude, Codex, and so on) from the Agents tab           | Agents in the sandbox have no login                             |

<CodeGroup>
  ```bash CLI theme={null}
  boat env set base --github true --secrets true --sandbox-credentials false --agents-credentials true
  ```

  ```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 '{"passGithub":true,"passSecrets":true,"passSandboxCredentials":false,"passAgentsCredentials":true}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.updateEnvironment({
    environmentId: envId,
    updateSandboxEnvironmentRequest: {
      passGithub: true,
      passSecrets: true,
      passSandboxCredentials: false,
      passAgentsCredentials: true,
    },
  });
  ```

  ```python Python theme={null}
  sandbox.update_environment(env_id, UpdateSandboxEnvironmentRequest(
      pass_github=True,
      pass_secrets=True,
      pass_sandbox_credentials=False,
      pass_agents_credentials=True,
  ))
  ```
</CodeGroup>

## Versions, and when a sandbox moves between them

This is the part worth reading twice.

```
you save a change  ──▶  a new version is created
                            │
                            ├──▶ every sandbox that starts from now on gets it
                            │
                            └──▶ Sandboxes already running: nothing happens
                                     │
                                     └──▶ they move only when you upgrade
```

A Sandbox takes the latest version **at the moment it starts**, and keeps that exact version for the rest of its life. Saving never reaches into a running sandbox. There is no automatic upgrade, no background rollout, and no scheduled window: a sandbox moves when you press **Upgrade** in the dashboard or run `boat env upgrade`, and at no other time.

`boat info` tells you which one a sandbox is on, as `environment` and `environmentVersion`. Compare that number against the environment's latest in `boat env list`: a sandbox below it is still running the older configuration, which is usually the answer to "I added that secret, why does my sandbox not have it?"

```bash theme={null}
boat info bx_f7k2q9hd     # env:  prod (v2)
boat env list             # prod  latest v3   -> this sandbox is one upgrade behind
```

Upgrading applies the new configuration and removes any secret the new version withholds. Live Sandboxes are cleaned and updated immediately; stopped sandboxes pick it up when they resume.

<Warning>
  Upgrading is not reversible on that sandbox's disk. A secret the new version withholds is deleted from the machine, not hidden. Re-pinning to the older version does not bring back a secret file the newer version dropped.
</Warning>

The Versions panel lists every version and how many sandboxes sit on each, so you can see what is still running old configuration.

## Repositories

<Note>
  Repositories need a GitHub connection on your account. If you signed in with Google or an email code, open [Dashboard > Environment](https://boat.dev/dashboard?tab=environment) and use **Connect GitHub** under GitHub repositories: it attaches GitHub to the account you already have, and you keep signing in the way you do now. Nothing else in Boat requires it: you can also skip the connection entirely and use `gh` with your own token inside the sandbox.
</Note>

Each repository carries a base branch and an optional setup script. Boat clones that branch as it is, and never creates a branch for you, forks included. Repositories stay on the base branch unless you or something inside the sandbox changes it.

<CodeGroup>
  ```bash CLI theme={null}
  boat env add-repo base octocat/hello-world --branch develop
  boat env rm-repo base octocat/hello-world
  ```

  ```bash curl theme={null}
  repo_id=$(curl -sS "$BOAT_API_BASE/repos?sync=true&q=hello-world" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    | jq -r '.installations[0].repositories[0].databaseId')

  curl -sS -X POST "$BOAT_API_BASE/repos" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"repositoryId\":\"$repo_id\",\"baseBranch\":\"develop\"}"
  ```

  ```ts TypeScript theme={null}
  const repos = await sandbox.repos({ sync: true, q: "hello-world" });
  const repo = repos.installations.flatMap((i) => i.repositories)[0];

  await sandbox.selectRepo({
    repoSelectionRequest: { repositoryId: repo.databaseId, baseBranch: "develop" },
  });
  ```

  ```python Python theme={null}
  from boat_sdk.models.repo_selection_request import RepoSelectionRequest

  repos = sandbox.repos(sync=True, q="hello-world")
  repo = repos.installations[0].repositories[0]

  sandbox.select_repo(RepoSelectionRequest(repository_id=repo.database_id, base_branch="develop"))
  ```
</CodeGroup>

<Note>
  `selectRepo` and `updateSecrets` act on your **default** environment. To edit a named one, use the environment calls below.
</Note>

### Where repositories land

On the hosted image the SSH user is `user` and the work directory is `/home/user`. Each folder is named after the repository, not the `owner/name` pair.

| GitHub repository                   | Boat path                       |
| ----------------------------------- | ------------------------------- |
| `ariana-dot-dev/ariana-ide-private` | `/home/user/ariana-ide-private` |
| `octocat/hello-world`               | `/home/user/hello-world`        |

With one repository, that folder is the project directory for agent tools. With several, `/home/user` stays the parent workspace and each repository is a sibling folder.

## Secrets

Two shapes, both injected when a sandbox starts.

* **Environment variables**, readable as process env vars and shell exports inside the sandbox.
* **Secret files**, written under `/home/user` at the relative path you give.

Use these for app credentials, API keys, `.env` files and deployment tokens. Do not pass secrets in prompts, URLs, CLI arguments that may be logged, Docker build args, or committed files.

<CodeGroup>
  ```bash CLI 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
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/secrets" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"envContents":"STRIPE_KEY=sk_live_123\n","secretFiles":[{"path":"backend/.env","contents":"DATABASE_URL=postgres://...\n"}]}'
  ```

  ```ts TypeScript theme={null}
  // default environment
  await sandbox.updateSecrets({
    secretsUpdateRequest: {
      envContents: "STRIPE_KEY=sk_live_123\n",
      secretFiles: [
        { path: "backend/.env", contents: "DATABASE_URL=postgres://...\n" },
      ],
    },
  });

  // any named environment
  await sandbox.updateEnvironment({
    environmentId: envId,
    updateSandboxEnvironmentRequest: {
      envContents: "STRIPE_KEY=sk_live_123\n",
      secretFiles: [{ path: "backend/.env", contents: "DATABASE_URL=postgres://...\n" }],
    },
  });
  ```

  ```python Python theme={null}
  from boat_sdk.models.secret_file import SecretFile
  from boat_sdk.models.secrets_update_request import SecretsUpdateRequest

  # default environment
  sandbox.update_secrets(SecretsUpdateRequest(
      env_contents="STRIPE_KEY=sk_live_123\n",
      secret_files=[SecretFile(path="backend/.env", contents="DATABASE_URL=postgres://...\n")],
  ))

  # any named environment
  sandbox.update_environment(env_id, UpdateSandboxEnvironmentRequest(
      env_contents="STRIPE_KEY=sk_live_123\n",
      secret_files=[SecretFile(path="backend/.env", contents="DATABASE_URL=postgres://...\n")],
  ))
  ```
</CodeGroup>

<Warning>
  The `/secrets` endpoint is a full **replacement**, not a merge. Send every variable and secret file that should remain, or the ones you leave out are dropped. The granular `boat env set-var` and `set-file` commands change one item at a time and do not have this hazard.
</Warning>

### Secret file paths

Paths are relative to `/home/user`. There is no repository picker, so include the repository folder name to land a file inside a clone:

```text theme={null}
ariana-ide-private/backend/.env
```

writes to:

```bash theme={null}
/home/user/ariana-ide-private/backend/.env
```

Absolute paths, and paths that escape `/home/user`, are skipped.

### Per-Sandbox variables

An environment's variables apply to every sandbox using it. To give one sandbox its own values, pass `env` when you create it. Per-Sandbox values are merged over the environment's, so a per-Sandbox key wins a name collision.

<CodeGroup>
  ```bash CLI theme={null}
  boat new --env DATABASE_URL=postgres://... --env FEATURE_FLAG=1
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ttlSeconds":3600,"env":{"DATABASE_URL":"postgres://...","FEATURE_FLAG":"1"}}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.create({
    createSandboxRequest: {
      ttlSeconds: 3600,
      env: { DATABASE_URL: "postgres://...", FEATURE_FLAG: "1" },
    },
  });
  ```

  ```python Python theme={null}
  sandbox.create(CreateSandboxRequest(
      ttl_seconds=3600,
      env={"DATABASE_URL": "postgres://...", "FEATURE_FLAG": "1"},
  ))
  ```
</CodeGroup>

Keys must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars), at most 100 variables and 64KB per sandbox. Reserved Boat-internal names (`ASCII_TOKEN`, `BOAT_ID`, and similar) are rejected. A forked sandbox inherits its source's per-Sandbox variables unless the fork passes its own `env`.

## Named environments

There is always exactly one default, and it is what a sandbox uses when you do not name one.

<CodeGroup>
  ```bash CLI theme={null}
  boat env list
  boat env info staging
  boat env new staging
  boat env rename staging prod
  boat env default prod
  boat env rm staging
  boat env upgrade prod
  ```

  ```bash curl theme={null}
  curl -sS "$BOAT_API_BASE/environments" -H "Authorization: Bearer $BOAT_API_KEY"

  curl -sS -X POST "$BOAT_API_BASE/environments" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"staging"}'

  curl -sS -X PUT "$BOAT_API_BASE/environments/$ENV_ID" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"prod","isDefault":true,"safeForThirdParties":true}'

  curl -sS -X POST "$BOAT_API_BASE/environments/$ENV_ID/upgrade" \
    -H "Authorization: Bearer $BOAT_API_KEY"

  curl -sS -X DELETE "$BOAT_API_BASE/environments/$ENV_ID" \
    -H "Authorization: Bearer $BOAT_API_KEY"
  ```

  ```ts TypeScript theme={null}
  const { environments } = await sandbox.environments();
  const staging = await sandbox.createEnvironment({
    createSandboxEnvironmentRequest: { name: "staging" },
  });

  await sandbox.updateEnvironment({
    environmentId: envId,
    updateSandboxEnvironmentRequest: { name: "prod", isDefault: true, safeForThirdParties: true },
  });

  await sandbox.upgradeEnvironment({ environmentId: envId });
  await sandbox.deleteEnvironment({ environmentId: envId });
  ```

  ```python Python theme={null}
  from boat_sdk.models.create_sandbox_environment_request import CreateSandboxEnvironmentRequest
  from boat_sdk.models.update_sandbox_environment_request import UpdateSandboxEnvironmentRequest

  envs = sandbox.environments()
  staging = sandbox.create_environment(CreateSandboxEnvironmentRequest(name="staging"))

  sandbox.update_environment(env_id, UpdateSandboxEnvironmentRequest(
      name="prod", is_default=True, safe_for_third_parties=True,
  ))

  sandbox.upgrade_environment(env_id)
  sandbox.delete_environment(env_id)
  ```
</CodeGroup>

Deleting is a soft delete. Sandboxes pinned to its versions keep running; new Sandboxes can no longer use it.

`upgradeEnvironment` accepts an optional list of sandbox ids to restrict the upgrade; omit it to move every sandbox of yours that is on an older version.

## Using an environment for a sandbox

Pass the name when you create, resume, or fork. Omit it to use your default. Unknown names are rejected outright, before anything is created or changed, so a typo costs you nothing. Environments are never created implicitly.

<CodeGroup>
  ```bash CLI theme={null}
  boat new --environment staging
  boat resume bx_f7k2q9hd --environment staging
  boat fork bx_f7k2q9hd --environment staging
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ttlSeconds":3600,"environment":"staging"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.create({ createSandboxRequest: { ttlSeconds: 3600, environment: "staging" } });
  await sandbox.resume({ sandboxId, resumeRequest: { environment: "staging" } });
  await sandbox.fork({ sandboxId, forkRequest: { environment: "staging" } });
  ```

  ```python Python theme={null}
  sandbox.create(CreateSandboxRequest(ttl_seconds=3600, environment="staging"))
  sandbox.resume(sandbox_id, ResumeRequest(environment="staging"))
  sandbox.fork(sandbox_id, ForkRequest(environment="staging"))
  ```
</CodeGroup>

<Warning>
  `--environment` and `--env` are different things. `--environment staging` picks which environment the sandbox uses. `--env KEY=value` sets one variable on that single sandbox, on top of whatever the environment gives it.
</Warning>

## Related

* [Setup & Scripts](/setup)
* [Platform guide](/platform-guide)
* [Snapshots & Copies](/snapshots)
* [CLI reference](/cli-reference)
