# Signing agents in

> Connect Claude Code, Codex and opencode in a space to your own subscription, or give them provider keys through an AI profile.

moatbox runs coding agents (Claude Code, Codex and opencode) inside spaces: Firecracker microVMs on moatbox servers or on a server your team links, each with its own disk. Before an agent can do any work it needs a model account. This page covers the two ways to give it one: sign the agent in to your own subscription inside a space, or give the space provider API keys through an AI profile. Starting runs is covered in [Running agents](https://docs.moatbox.dev/agents.md).

## Pick a method

| | Subscription sign-in | AI profile key |
|---|---|---|
| What you connect | Your own Claude or ChatGPT plan | A provider API key or custom API |
| Where the credential lives | A login file in the agent's home directory, on the space's disk | Encrypted in moatbox; the space gets a placeholder |
| Scope | One agent in one space | Every space whose environment uses the profile |
| Who sets it up | Anyone who can start runs in the space | Team owners and admins |

You can use both. The AI page shows the sign-in state and the profile keys side by side for each agent.

## Which accounts each agent accepts

The dashboard offers these accounts per agent:

| Agent | Subscription sign-in | Keys from an AI profile |
|---|---|---|
| Claude Code | Claude subscription | Anthropic API key, AWS Bedrock |
| Codex | ChatGPT subscription | OpenAI API key |
| opencode | ChatGPT subscription | Anthropic API key, OpenAI API key |

When a profile has AWS Bedrock and no other Anthropic credential, moatbox sets `CLAUDE_CODE_USE_BEDROCK=1` in the space so Claude Code talks to Bedrock.

## Sign in to a subscription from the dashboard

The **Sign in** button runs the agent's own login command inside your space, the same command you would type in its terminal:

| Agent | Command moatbox runs in the space |
|---|---|
| Claude Code | `claude auth login --claudeai` |
| Codex | `codex login --device-auth` |
| opencode | `opencode auth login -p openai -m "ChatGPT Pro/Plus (headless)"` |

For Claude Code this is the unmodified Claude Code login, running inside your own space. moatbox shows you the URL and code the command prints and passes back the code you paste. The login file the command writes stays in that space.

You can start a sign-in from two places:

- **AI** in the sidebar. Pick the space in the sidebar first; the page shows **Agents in** your space. Open an agent's row and press **Sign in** (or **Sign in again** if it already shows **Signed in**). If you have no plan yet, **Get a plan ↗** links to the provider.
- The run composer. When the chosen agent has no login in the space, the composer shows "Claude Code isn't signed in to this space yet, so it can't run." with a **Sign in to Claude Code** button (the agent name changes with your choice).

Both open the same panel, **Sign in to `<agent>` in this space**:

1. The panel shows "Starting Claude Code's sign-in…" while the space starts the command. If the space is asleep, starting the sign-in wakes it.
2. Press **Open `<host>` ↗** and approve the request on the provider's page.
3. Then one of two things happens:
   - If the command printed a device code (Codex), the panel shows it with a **Copy** button. Enter it on the provider's page. The panel updates by itself once you finish.
   - Otherwise (Claude Code), the provider's page shows you a code. Paste it into the **Code** field and press **Sign in**. The button reads **Checking…** while the agent verifies it. If the agent rejects the code, the panel shows its message and you can paste another.
4. When the command exits with status 0, the panel closes and the agent shows **Signed in**.

If the command ends without signing in, the panel shows the agent's last message and a **Try again** button. **Cancel** stops the sign-in.

Codex signs in with a device code. Turn on device code login in your ChatGPT settings before you start.

Starting a sign-in adds a "Signed in to `<agent>`" entry to the team audit log.

## Sign in over the API

A sign-in is a run with `"login": true` and no task. Your API key needs the `run` scope to start it and send the code, and `space.read` to read its progress.

```bash
curl -s -X POST https://app.moatbox.dev/api/v1/spaces/<space>/runs \
  -H "Authorization: Bearer mbx_xxx" \
  -H "Content-Type: application/json" \
  -d '{"agent": "codex", "login": true}'
```

The answer holds the run `id`. Poll its timeline and look for these event kinds:

| Event kind | Summary holds |
|---|---|
| `login.url` | The URL to open |
| `login.code` | A device code to enter on that page |
| `login.output` | A line from the login command, such as an error or a success message |

```bash
curl -s https://app.moatbox.dev/api/v1/spaces/<space>/runs/<run-id>/timeline \
  -H "Authorization: Bearer mbx_xxx"
```

If the provider gave you a code to paste (Claude Code), send it as a message:

```bash
curl -s -X POST https://app.moatbox.dev/api/v1/spaces/<space>/runs/<run-id>/messages \
  -H "Authorization: Bearer mbx_xxx" \
  -H "Content-Type: application/json" \
  -d '{"text": "<code-from-the-provider>"}'
```

The run ends with exit 0 when the login succeeds. To check which agents have a login in a space:

```bash
curl -s https://app.moatbox.dev/api/v1/spaces/<space>/logins \
  -H "Authorization: Bearer mbx_xxx"
```

```json
{"claude": true, "codex": false, "opencode": false}
```

## Where the login lives

Each agent in a space has its own home directory, mounted at `/home/agent` when that agent runs. All agents share `/workspace`. A sign-in writes the agent's login file into its own home:

| Agent | Login file |
|---|---|
| Claude Code | `~/.claude/.credentials.json` (plus the account entry in `~/.claude.json`) |
| Codex | `~/.codex/auth.json` |
| opencode | `~/.local/share/opencode/auth.json` |

Some consequences:

- **Per space and per agent.** Signing Claude Code in to one space does nothing for your other spaces, or for Codex in the same space. Sign in once in each space where you run that agent.
- **Sleep and wake keep it.** The files sit on the space's disk, so a space that sleeps and wakes stays signed in.
- **Snapshots include it.** A snapshot copies the whole disk. Restoring one puts the login files back as they were when the snapshot was taken.
- **Shared snapshots strip it.** When you share a snapshot with another team, moatbox removes sign-in files (Claude, Codex, opencode, GitHub, cloud and SSH keys) from the copy, and you see the list before you send the code. See [Snapshots, restore and undo](https://docs.moatbox.dev/snapshots.md).
- **Reset deletes it, unless you keep it.** A reset empties `/workspace` and `/home/agent`. Tick **Keep logins** in the reset form (or pass `--keep-logins` to `moatbox reset`) and moatbox copies the login files above, and the GitHub CLI's `~/.config/gh/hosts.yml`, out of the space before the reset and writes them back after. From `~/.claude.json` it keeps the account entry and nothing else. If writing a login back fails, moatbox undoes the whole reset.

## Sign in from an SSH shell

When you connect to a space over SSH (see [Connect to a space](https://docs.moatbox.dev/connect.md)), the space's workspace mode decides where you land. In **Full machine** mode you are the `dev` user in the VM, with the home `/home/dev`. In **Locked** mode you land in a separate workspace container with its own `/home/agent`. Both share `/workspace` with the agents. Agents you start from that shell, including from the Claude desktop app or the Codex app pointed at the space's SSH host, use the shell's home. Runs you start from the dashboard or the API use the per-agent homes from the previous section.

So the two logins are separate. To use your subscription from an SSH shell, sign in there with the agent's own command:

```bash
ssh moatbox-<space>
claude            # open the URL it prints and finish the login
codex login --device-auth
```

`moatbox connect --for claude` or `--for codex` prints these steps for you. `claude remote-control` needs a subscription login; an API key does not work for it.

**Keep logins** on a reset copies files from the per-agent homes. It does not list the SSH shell's home, so sign in again in your SSH shell after a reset.

## Use API keys through an AI profile

An AI profile is a named set of provider keys and custom APIs that belongs to your team. Spaces never receive the real keys. Each space gets a placeholder (`mbx-ai-…`) in the provider's usual environment variable, plus a base URL that points at the moatbox AI gateway. The agent sends its requests to the gateway with the placeholder as its key. The gateway looks up the space's profile, puts the real key on the request, forwards it to the provider and counts the tokens.

Every team starts with a profile named `default`. Owners and admins manage profiles on the **AI** page, from a signed-in dashboard session; API keys cannot change them.

### Create a profile

1. Open **AI** and press **New profile**.
2. Enter a **Name** (a-z, 0-9, `-` and `_`, up to 40 characters, starting with a letter or digit).
3. Under **Copy keys and APIs from**, pick **Start empty** or an existing profile.
4. Press **Create**.

### Add a provider key

1. On the profile's card, press **Add key**.
2. Choose the **Provider** and give the key a **Label** (default `main`).
3. Paste the **Key**. For AWS Bedrock, give a **Region** and either an **Access key ID** and **Secret access key**, or a Bedrock API key.
4. Press **Save key**.

moatbox stores the key encrypted and shows its last 4 characters. The first key you add for a provider is in use at once. Later keys for the same provider wait as spares until you press **Use this one**. **Forget** deletes a key; if it was in use, the newest other key for that provider takes over.

You can also add a key from an agent's row in the **Agents** section of the AI page with the **+** button. Those rows show the keys of the default profile.

### Supported providers

| Provider | Variables the space gets |
|---|---|
| Anthropic API key | `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL` |
| OpenAI API key | `OPENAI_API_KEY`, `CODEX_API_KEY`, `OPENAI_BASE_URL` |
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_BASE_URL` |
| Google Gemini | `GEMINI_API_KEY`, `GOOGLE_GEMINI_BASE_URL` |
| DeepSeek | `DEEPSEEK_API_KEY`, `DEEPSEEK_BASE_URL` |
| Mistral | `MISTRAL_API_KEY`, `MISTRAL_BASE_URL` |
| xAI | `XAI_API_KEY`, `XAI_BASE_URL` |
| Moonshot (Kimi) | `MOONSHOT_API_KEY`, `MOONSHOT_BASE_URL` |
| AWS Bedrock | `AWS_BEARER_TOKEN_BEDROCK`, `ANTHROPIC_BEDROCK_BASE_URL`, `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, `AWS_REGION` |

Every active provider on the profile gets its variables, whether an agent uses that provider or not. Your own code and tools in the space can call the gateway with them too.

### Add a custom API

**Add custom API** routes any HTTPS API with a key through the gateway. Give it a **Name** such as `ACME`, a **Base URL** on a public host that no built-in integration covers, say where the key goes (**A header**, with header name and prefix, or **The query string**, with the parameter name), paste the key and press **Save API**. The name takes A-Z, 0-9 and `_`, up to 64 characters, starts with a letter, can't end in `_BASE_URL` and can't reuse a built-in variable name. Spaces get `ACME` (the placeholder) and `ACME_BASE_URL` (the gateway). Send the placeholder where the key goes; the gateway swaps in the real key.

### Choose which profile a space uses

A space uses the profile its environment names, or the team's default profile. Set it in the environment's **AI** section with the **Profile** select (see [Environments](https://docs.moatbox.dev/environments.md)). An environment marked **Safe for third parties** gets no profile unless you pick one there.

To use another profile for a single run, pass its name as `ai_profile` when you start the run through the API. That run gets its own placeholder, valid for 24 hours, and the audit entry for the run names the profile. See [Running agents](https://docs.moatbox.dev/agents.md) for the rest of the run request.

SSH shells in the space get the same variables as runs. The gateway reads the profile on every request, so a key you add, switch or forget takes effect at once. When a provider or custom API is added or removed, new shells and runs get the new set of variables.

### Watch and control a space's AI

Each space has an **AI usage** page with an **AI for this space** panel. It shows the space's **Profile**, its **Keys**, and **Inside**: whether the gateway works from inside the space. Owners and admins can:

- Turn **Allowed** off to block AI for the space. The gateway then refuses its requests.
- Press **New placeholder** to replace the space's placeholder. The old one stops working at once, wherever it was copied. A run started with `ai_profile` keeps its own placeholder until its 24 hours run out.
- Set a daily or monthly **Spend cap for this space**. The team cap on the AI page applies as well.

At a cap, the gateway refuses new model requests with an error the agent reads, and owners and admins get an alert. The request that crosses the cap still completes. If a provider rejects a key, the space records an "AI provider refused a key" event that names the profile and key label; check or replace the key on the AI page. See [Events, webhooks and telemetry](https://docs.moatbox.dev/events-and-webhooks.md).

## Related pages

- [Running agents](https://docs.moatbox.dev/agents.md): start, steer and resume runs.
- [Connect to a space](https://docs.moatbox.dev/connect.md): reach a space over SSH.
- [Environments](https://docs.moatbox.dev/environments.md): environment settings, including the AI profile.
- [Snapshots, restore and undo](https://docs.moatbox.dev/snapshots.md): snapshots, restore and sharing.
