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

# Agent in a box

> Run a complete agent — its file access, command execution, and package installs — inside a microVM instead of on your host.

An autonomous agent decides for itself what to run and what to modify. On the host that hands an untrusted model both the decision and the write access. Put the agent inside a box and the host exposes one boundary: stdin/stdout and forwarded ports.

## Pages in this section

| Page                                             | What it covers                                                                           |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| [Run Claude Code](/agent-in-box/run-claude-code) | `SkillBox` — the ready-made path — plus installing the CLI into a `SimpleBox` yourself   |
| [Run Codex](/agent-in-box/run-codex)             | OpenAI's CLI. Its own sandbox switch is documented for externally sandboxed environments |
| [Run Pi](/agent-in-box/run-pi)                   | Structured JSON output, and a custom-provider path for self-hosted endpoints             |
| [Run OpenCode](/agent-in-box/run-opencode)       | Whole configuration injected as JSON — nothing written into the image                    |
| [Run Hermes](/agent-in-box/run-hermes)           | A prebuilt image with its own runtime — one-shot prompts or the messaging gateway        |

The inverse arrangement — your loop on the host, the box as a tool it calls — is [Drive a sandbox from your agent loop](/agent-tools/drive-from-agent-loop). To reach boxes running on another machine, see [Manage remote sandboxes over REST](/guides/agent-service-endpoint).

***

## Section Navigation

This section layers the "agent in Box" capability by control granularity. Each row below corresponds to one implementation approach:

| Approach                            | When to use                                                                           | Primary SDK entry point                         | Control granularity                                |
| ----------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------- |
| `SkillBox.call(prompt)`             | Simplest path: single-line conversation, auto-installs the Claude CLI + skills        | `SkillBox.call` / `install_skill`               | High level: pass a prompt, get back a string reply |
| `SkillBox(skills=[...])`            | Pre-install skill packs for the agent before conversing                               | `SkillBox(skills=...)`                          | High level + skill provisioning                    |
| Build your own Box running `claude` | Full control over image, CLI flags, the stream-json protocol, and multi-turn sessions | `SimpleBox` / `Box` + `box.exec("claude", ...)` | Low level: manage stdin/stdout directly            |

> Link convention: this page links only to pages that actually exist in docs-v2. For the full parameters and defaults of each approach above, see [Agent Tools](/agent-tools/index) and [Box types](/manage-sandbox/sandbox-types).

***

## Choosing Between SkillBox and a Self-Built Box

| Dimension         | `SkillBox` (recommended starting point)                                                                                                                                                                         | Self-built `SimpleBox` + `box.exec("claude", ...)`                                 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Image             | **Defaults to** `ghcr.io/boxlite-ai/boxlite-skillbox:0.1.0` (can be overridden with `image=`, but the lazy-install logic assumes that image's Ubuntu/webtop environment, so overriding is an advanced use case) | Choose your own (e.g. `node:20-alpine`, then `npm i -g @anthropic-ai/claude-code`) |
| CLI installation  | **Lazily installed** on the first `call()`: Claude CLI + bash/git/python                                                                                                                                        | Install it yourself via `exec`                                                     |
| Conversation      | `await skill_box.call(prompt) -> str`, automatically maintaining multi-turn context                                                                                                                             | Drive the `--input-format stream-json` protocol and parse NDJSON yourself          |
| Default resources | memory 4096 MiB, disk 10 GB, noVNC ports 3000/3001                                                                                                                                                              | Custom `memory_mib` / `disk_size_gb`                                               |
| Best for          | Quickly running a tool-capable Claude inside an isolated VM                                                                                                                                                     | Fine-grained control over CLI flags, protocol, and session lifecycle               |

> Only Python and Node provide the high-level `SkillBox` wrapper; C / Go / Rust must use the low-level `exec` to drive the CLI themselves.

***

## Quick Example (Minimal Happy Path)

### Python: Run a complete agent inside a Box with SkillBox (simplest path)

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import os

from boxlite import SkillBox

async def main():
    # SkillBox requires a Claude Code OAuth token.
    # Read from the environment variable first; may also pass oauth_token=...
    if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
        # Without a token, entering async with raises ValueError; surface a helpful hint early
        print("Please set it first: export CLAUDE_CODE_OAUTH_TOKEN=<YOUR_OAUTH_TOKEN>")
        return

    try:
        # Image defaults to boxlite-skillbox; no need to pass image
        # auto_remove defaults to True: exiting async with cleans up the entire VM
        async with SkillBox() as skill_box:
            # id is a property and info() is a synchronous method; do not await either
            print("Box ID:", skill_box.id)

            # The first call() triggers lazy installation (Claude CLI / bash / git / python)
            # call(prompt) returns Claude's final reply (str)
            answer = await skill_box.call("What is 2 + 2? Just give the number.")
            print("Claude:", answer)

            # Multi-turn conversation: context is maintained automatically within the same SkillBox session
            await skill_box.call("My name is Alice.")
            who = await skill_box.call("What is my name? One word.")
            print("Claude:", who)
    except ValueError as e:
        # For example, a missing oauth_token
        print("Configuration error:", e)
    except RuntimeError as e:
        # Image pull failure / no hardware virtualization, etc. raise a standard RuntimeError (not BoxliteError)
        print("Startup or pull failed:", e)

asyncio.run(main())
```

### Python: Pre-install skill packs for the agent

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import os

from boxlite import SkillBox

async def main():
    if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
        print("Please set it first: export CLAUDE_CODE_OAUTH_TOKEN=<YOUR_OAUTH_TOKEN>")
        return

    try:
        # skills are installed alongside the Claude CLI on the first call()
        async with SkillBox(skills=["anthropics/skills"]) as skill_box:
            answer = await skill_box.call("List the top 3 skills you have, briefly.")
            print(answer)

            # A skill can also be installed manually at runtime; returns a bool indicating success
            ok = await skill_box.install_skill("anthropics/skills")
            print("install_skill ->", ok)
    except (ValueError, RuntimeError) as e:
        print("Failed:", e)

asyncio.run(main())
```

### Node: Run an agent with SkillBox

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Package name is @boxlite-ai/boxlite (not 'boxlite', not '@boxlite/sdk')
import { SkillBox } from "@boxlite-ai/boxlite";

async function main(): Promise<void> {
  if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
    console.error("Please set it first: export CLAUDE_CODE_OAUTH_TOKEN=<YOUR_OAUTH_TOKEN>");
    return;
  }

  try {
    // await using: at end of scope, cleans up automatically per autoRemove (default true) by calling stop()
    // Note: Node's SkillBox does not auto-start() on construction or await using;
    // you must explicitly await skillBox.start(), otherwise call() throws "SkillBox not started."
    await using skillBox = new SkillBox();
    await skillBox.start();

    // The first call() triggers lazy installation; returns Claude's reply (string)
    const answer = await skillBox.call("What is 2 + 2? Just give the number.");
    console.log("Claude:", answer);
  } catch (e) {
    // On Node, a missing token makes start() throw a plain Error("OAuth token required...");
    // pull failure / no virtualization also throw a plain Error (instanceof BoxliteError === false)
    console.error("Failed:", e);
  }
}

main();
```

### Python: Build your own Box for full control over the Claude CLI (advanced)

When you need to choose your own image, customize CLI flags, or drive the stream-json protocol directly for multi-turn sessions, use the low-level `Box` to run `claude` yourself. Below is a minimal, self-contained skeleton (for the full protocol details, see the repository example `examples/python/06_ai_agents/chat_with_claude.py`).

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import json
import os

import boxlite

async def main():
    token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "")
    if not token:
        print("Please set it first: export CLAUDE_CODE_OAUTH_TOKEN=<YOUR_OAUTH_TOKEN>")
        return

    try:
        with boxlite.Boxlite.default() as runtime:
            from boxlite import BoxOptions

            options = BoxOptions(
                image="node:20-alpine",
                memory_mib=2048,
                disk_size_gb=5,
                auto_remove=False,                       # Persist to reuse the already-installed CLI
                # The native Box env is list[tuple[str, str]] (note: not a dict)
                env=[("CLAUDE_CODE_OAUTH_TOKEN", token)],
            )

            # runtime.get_or_create returns an awaitable through the wrapper layer
            box, _created = await runtime.get_or_create(options, name="claude-box")
            async with box:
                # Install the Claude Code CLI (native Box.exec returns an Execution; await wait())
                install = await box.exec(
                    "npm", ["install", "-g", "@anthropic-ai/claude-code"], None
                )
                result = await install.wait()
                if result.exit_code != 0:
                    print("Failed to install Claude CLI")
                    return

                # Start Claude with the stream-json protocol and take over stdin/stdout yourself
                proc = await box.exec(
                    "claude",
                    ["--input-format", "stream-json",
                     "--output-format", "stream-json", "--verbose"],
                    [("CLAUDE_CODE_OAUTH_TOKEN", token)],
                )
                stdin = proc.stdin()
                stdout = proc.stdout()

                msg = {
                    "type": "user",
                    "message": {"role": "user", "content": "Say OK."},
                    "session_id": "default",
                    "parent_tool_use_id": None,
                }
                await stdin.send_input((json.dumps(msg) + "\n").encode())

                # Read and parse NDJSON until a type=="result" message arrives
                buffer = ""
                while True:
                    chunk = await asyncio.wait_for(stdout.__anext__(), timeout=120)
                    buffer += chunk.decode() if isinstance(chunk, bytes) else chunk
                    while "\n" in buffer:
                        line, buffer = buffer.split("\n", 1)
                        if not line.strip():
                            continue
                        parsed = json.loads(line)
                        if parsed.get("type") == "result":
                            print("Claude:", parsed.get("result", ""))
                            await stdin.close()
                            await proc.wait()
                            return
    except RuntimeError as e:
        print("Startup or pull failed:", e)

asyncio.run(main())
```

***

## Parameters and Returns (Core Entry Points)

This is a navigation page; it lists only the **core entry points and return values** for placing an agent inside a Box. For full parameters, see each sub-page and [Agent Tools](/agent-tools/index).

### `SkillBox` (Python, inherits from `SimpleBox`)

| Parameter        | Required/Optional | Type                 | Default           | Description                                                                                              |
| ---------------- | ----------------- | -------------------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
| `skills`         | Optional          | `list[str]` / `None` | `None`            | Skill packs installed alongside the CLI on the first `call()`                                            |
| `oauth_token`    | Optional          | `str` / `None`       | `None`            | Falls back to the `CLAUDE_CODE_OAUTH_TOKEN` env var; if neither is set, `__aenter__` raises `ValueError` |
| `name`           | Optional          | `str`                | `"skill-box"`     | Box name                                                                                                 |
| `memory_mib`     | Optional          | `int`                | `4096`            | Memory                                                                                                   |
| `disk_size_gb`   | Optional          | `int`                | `10`              | Disk                                                                                                     |
| `gui_http_port`  | Optional          | `int`                | `0` (random port) | Host port mapped to the noVNC HTTP endpoint                                                              |
| `gui_https_port` | Optional          | `int`                | `0` (random port) | Host port mapped to the noVNC HTTPS endpoint                                                             |
| `auto_remove`    | Optional          | `bool`               | `True`            | Whether to delete the Box automatically when exiting `async with`                                        |

> The image defaults to `ghcr.io/boxlite-ai/boxlite-skillbox:0.1.0`. It can be overridden with `image=`, but the lazy-install logic assumes that image's Ubuntu/webtop environment, so overriding is an advanced use case.

| Method             | Signature                      | Returns   | Description                                                       |
| ------------------ | ------------------------------ | --------- | ----------------------------------------------------------------- |
| `call`             | `call(prompt)`                 | `str`     | Multi-turn conversation; returns Claude's final reply             |
| `install_skill`    | `install_skill(skill_id)`      | `bool`    | Install a skill manually at runtime; returns whether it succeeded |
| `wait_until_ready` | `wait_until_ready(timeout=60)` | —         | Wait until the Box is ready                                       |
| `info`             | `info()`                       | `BoxInfo` | **Synchronous**; do not await                                     |

### Self-Built Box Running Claude (Low-Level Entry Points)

| Entry point          | Signature (key parameters)                                                              | Returns                     | Notes                                                                          |
| -------------------- | --------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| `Box.exec` (native)  | `exec(command, args=None, env=None, tty=False, user=None, timeout_secs=None, cwd=None)` | `Execution`                 | `env` is `list[tuple[str,str]]`; the timeout parameter is named `timeout_secs` |
| `Execution.stdin()`  | —                                                                                       | `ExecStdin` (may be `None`) | Use `send_input(bytes)` to write, `close()` to send EOF                        |
| `Execution.stdout()` | —                                                                                       | Async iterator              | `async for chunk in stdout` or `await stdout.__anext__()`                      |
| `Execution.wait()`   | `wait()`                                                                                | `ExecResult`                | Read the exit code (the native `ExecResult` does not carry stdout/stderr)      |

### Accessing `BoxInfo` State

| Access path               | Type           | Description                                                        |
| ------------------------- | -------------- | ------------------------------------------------------------------ |
| `box.info()`              | `BoxInfo`      | Get info synchronously                                             |
| `box.info().state`        | `BoxStateInfo` | State object (note it is `state`; the outer field is not `status`) |
| `box.info().state.status` | `str`          | State string (the inner field name is `status`)                    |

***

## Troubleshooting

### Missing OAuth token: entering `async with` raises `ValueError`

`SkillBox` checks for the token in `__aenter__`. When both the `oauth_token` parameter and the `CLAUDE_CODE_OAUTH_TOKEN` environment variable are missing, it raises `ValueError`. Set it first:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export CLAUDE_CODE_OAUTH_TOKEN="<YOUR_OAUTH_TOKEN>"   # replace with your real token
```

Or pass it in code: `SkillBox(oauth_token="<YOUR_OAUTH_TOKEN>")`.

### Passing `image` to `SkillBox` breaks Claude installation

The `SkillBox` image **defaults** to `ghcr.io/boxlite-ai/boxlite-skillbox:0.1.0`. You **can** override it at construction with `image=` (Node has an `image` option too), but the lazy-install logic on the first `call()` always uses the Ubuntu/apt-get flow and installs Claude into `/config/.local/bin` (the home directory of webtop's `abc` user). Switching to a non-matching image such as `node:20-alpine` causes installation to fail. If you need a fully custom image to run Claude, use `SimpleBox(image=...)` / the low-level `Box` instead and `exec` the CLI installation yourself (see the "Build your own Box" example above).

### Mistakenly awaiting `box.info()` as if it were async

`info()` is a **synchronous** method (it does not touch the VM). Writing `await box.info()` raises an error (such as `TypeError: object BoxInfo can't be used in 'await' expression`). Call `box.info()` directly; access state through `box.info().state.status`.

### Passing the native `Box.exec` `env` as a dict

A self-built Box uses the **native** `Box.exec`, whose `env` must be `list[tuple[str,str]]` (e.g. `[("KEY", "value")]`), not a dict. Only the wrapper-layer `SimpleBox.exec` accepts a dict.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Correct (native Box):
await box.exec("claude", ["--version"], [("CLAUDE_CODE_OAUTH_TOKEN", token)])
```

### Assuming a failed command raises an exception

When a command exits with a non-zero code, `exec` **does not raise**. At the native layer, call `await execution.wait()` to get an `ExecResult` and check `exit_code` yourself:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
result = await execution.wait()
if result.exit_code != 0:
    print("CLI install/run failed")
```

In contrast, a **missing command** or **image pull failure** raises a **standard `RuntimeError`** (on Node, a **plain `Error`** where `instanceof BoxliteError === false`). Use a broad `except RuntimeError` / `catch` as a fallback rather than catching only `BoxliteError`.

### Box network access fails when pulling models or installing packages

A Box enables networking by default (`NetworkSpec` defaults to `Enabled{allow_net: []}`; an empty allowlist means traffic is permitted). If you have explicitly tightened `network`, allow the domains Claude / npm need; see [Network Access](/manage-sandbox/network-access).

### Writing the token into the image or command logs

An OAuth token is a sensitive credential. In production, inject it via [Secrets](/manage-sandbox/secrets-and-security) (`BoxOptions(secrets=[Secret(...)])`) rather than hard-coding it into `env` or printing it to logs.

### Startup failure: missing hardware virtualization (environment constraint)

BoxLite requires hardware virtualization to boot a microVM:

* **Linux**: requires KVM (`/dev/kvm` accessible; WSL2 needs KVM enabled and the user in the `kvm` group).
* **macOS**: uses Apple's Hypervisor.framework, **no `/dev/kvm` required**. macOS Intel is not supported.
* **Environments without virtualization** (some containers / CI): `start()` fails and raises, but the process stays alive; catch it with `try/except`.

Platform support: macOS ARM64 (supported) · Linux x86\_64 (supported) · Linux ARM64 (supported) · Windows WSL2 (supported) · macOS Intel (not supported).

***

## Comparison: Using a Box as an Agent's "Tool" (Instead of Putting the Agent in the Box)

If the agent loop runs on the **host** and only delegates the "run a command" step to a Box (the LLM tool-use pattern), that shape does not belong to this section; it belongs to [Agent Tools](/agent-tools/index). Its shape is:

* Start a sandbox on the host with `SimpleBox(image=...)`.
* Expose a `sandbox_exec` tool to the LLM that internally calls `await box.exec(*argv)`.
* The LLM decides which commands to run, and the results are fed back to the model to continue reasoning.

The difference: **this section is "the whole agent inside a Box"** (the only boundary is stdin/stdout/ports), whereas **the comparison pattern is "the agent outside, the Box as its means of execution"** (the host holds the full agent logic). Choose one based on your trust boundary and control requirements.

***

## Next Steps

* Learn about the agent's capabilities inside a Box (exec / file I/O / terminal / browser); see [Agent Tools](/agent-tools/index).
* If you have not yet built a mental model of the Box lifecycle, read [Manage Sandbox](/manage-sandbox/index) first.
* Learn the positioning and default resources of each Box type; see [Box types](/manage-sandbox/sandbox-types).
* Inject credentials securely for your agent; see [Secrets and Security](/manage-sandbox/secrets-and-security).
