# Sandboxed agents
URL: /docs/agents/sandboxed-agents
LLM index: /llms.txt
Description: Hand an agent in an untrusted sandbox one instance to drive, while instance lifecycle and your API key stay outside.

# Sandboxed agents

`LIM_API_KEY` is an organization-wide credential: it can create, list, and delete every instance you have. An agent in an untrusted or shared sandbox should not hold it. Create the instance outside the sandbox and hand the agent only that instance's own URL and token. The `lim` CLI drives a pinned instance without any API key.

## Create the instance and capture its credentials

Run the create where the API key lives: your machine, the orchestrator, or CI. Every instance returns its own `status.apiUrl` and `status.token`:

```bash
lim ios create --json > instance.json
jq -r .status.apiUrl instance.json    # the instance's API URL
jq -r .status.token  instance.json    # the instance's token
```

If the orchestrator is a backend service, the TypeScript SDK makes the same call:

```ts
import Limrun from '@limrun/api';

const lim = new Limrun({ apiKey: process.env.LIM_API_KEY });
const instance = await lim.iosInstances.create({ wait: true });
// Hand instance.status.apiUrl and instance.status.token to the sandbox.
```

Create each resource as the agent needs it:

| Resource | CLI | TypeScript SDK |
|---|---|---|
| iOS simulator | `lim ios create --json` | `lim.iosInstances.create({ wait: true })` |
| Android emulator | `lim android create --no-connect --json` | `lim.androidInstances.create({ wait: true })` |
| Xcode sandbox | `lim xcode create --json` | `lim.xcodeInstances.create({ wait: true })` |
| Gradle sandbox | `lim gradle create --json` | `lim.gradleInstances.create({ wait: true })` |

`--no-connect` skips the local ADB tunnel, whose output would otherwise mix into the JSON. `wait: true` returns once the instance is ready.

## Point the sandboxed CLI at the instance

Export the pair inside the sandbox. Every `lim ios` command picks them up directly, and nothing is persisted:

```bash
export LIM_IOS_INSTANCE_URL=https://.../v1/ios_.../api
export LIM_IOS_INSTANCE_TOKEN=<instance-token>

lim ios element-tree    # works with no LIM_API_KEY
```

To pin the instance once per workspace instead, run `set-instance`, which stores it in `~/.lim/last-instances.json`:

```bash
lim ios set-instance --api-url https://.../v1/ios_.../api --token <instance-token>
```

Both forms exist for every instance type:

| Instance type | Environment variables | Pin command |
|---|---|---|
| iOS simulator | `LIM_IOS_INSTANCE_URL`, `LIM_IOS_INSTANCE_TOKEN` | `lim ios set-instance` |
| Android emulator | `LIM_ANDROID_INSTANCE_URL`, `LIM_ANDROID_INSTANCE_TOKEN` | `lim android set-instance` |
| Xcode sandbox | `LIM_XCODE_INSTANCE_URL`, `LIM_XCODE_INSTANCE_TOKEN` | `lim xcode set-instance` |
| Gradle sandbox | `LIM_GRADLE_INSTANCE_URL`, `LIM_GRADLE_INSTANCE_TOKEN` | `lim gradle set-instance` |

If the agent should run `lim android connect`, also pass the ADB WebSocket URL (`status.adbWebSocketUrl`) as `LIM_ANDROID_INSTANCE_ADB_URL` or with the `--adb-websocket-url` flag of `lim android set-instance`.

When more than one is present, an explicit `--id` wins over the environment pair, which wins over the pinned instance.

## Build without the API key

`lim xcode build` targets an Xcode sandbox, which is its own resource with its own credentials. Create it where the key lives:

```bash
lim xcode create --json > xcode.json
```

Then, inside the sandbox, export its pair and build:

```bash
export LIM_XCODE_INSTANCE_URL=$(jq -r .status.apiUrl xcode.json)
export LIM_XCODE_INSTANCE_TOKEN=$(jq -r .status.token xcode.json)

lim xcode build .       # remote build, still no API key
```

`lim gradle build` follows the same pattern with `lim gradle create --json` and the `LIM_GRADLE_INSTANCE_URL` and `LIM_GRADLE_INSTANCE_TOKEN` pair.

## Share files through signed asset URLs

[Asset Storage](/docs/platform/asset-storage) lives on the management API, so the sandbox cannot mint asset URLs itself. Create the asset outside and hand the sandbox its signed URLs, which carry their own authorization.

To let the agent ship a build artifact, create the asset where the key lives and pass its upload URL in:

```bash
: > agent-build.zip                     # empty placeholder; the agent's upload replaces it
lim asset push ./agent-build.zip
lim asset list --name agent-build.zip --upload-url --json | jq -r '.[0].signedUploadUrl'
```

From the TypeScript SDK, one call returns both URLs without uploading anything:

```ts
const asset = await lim.assets.getOrCreate({ name: 'agent-build.zip' });
// Hand asset.signedUploadUrl to the sandbox. The same call also returns
// asset.signedDownloadUrl for later.
```

In the sandbox, the build uploads straight to that URL without touching the management API:

```bash
lim xcode build . --signed-upload-url "$UPLOAD_URL"
```

To hand the agent a file, such as a prebuilt app or a fixture, push it outside with `lim asset push ./MyApp.app.zip` and read its download URL with `lim asset list --name MyApp.app.zip --download-url --json`. The agent installs it without a key, and the instance downloads the URL server-side:

```bash
lim ios install-app "$DOWNLOAD_URL"
```

## What still needs the key

Anything on the management API fails with an authentication error inside the sandbox: creating, listing, and deleting instances, and minting asset URLs the way `lim xcode build --upload` does. That is the point. The agent drives the one instance it was given, and lifecycle stays with whoever holds the key. Delete the instance from the orchestrator when the work is done.

## Next steps

<Columns cols={2}>
  <Card title="Per-instance MCP server" icon="plug" href="/docs/agents/mcp">
    Give the same instance token to an agent that prefers MCP tool calls.
  </Card>
  <Card title="Concepts" icon="book-open" href="/docs/concepts">
    How API keys, instance tokens, and signed stream URLs relate.
  </Card>
  <Card title="CLI reference" icon="terminal" href="/docs/reference/cli">
    Shared flags and every environment variable the CLI reads.
  </Card>
</Columns>