---
title: "Sandbox"
description: "The agent's isolated bash environment: built-in file tools, a seeded /workspace, backends, lifecycle, and network policy."
---

The sandbox is the agent's isolated bash environment: a filesystem rooted at `/workspace` where it can run shell commands, execute scripts, and read or write files without ever touching your app runtime. Every Eve agent has exactly one. The built-in `bash`, `read_file`, `write_file`, `glob`, and `grep` tools already target it, and your authored code can too.

You author nothing for the agent to have a working sandbox; it exists by default. Override it only when you need to add setup, seed files, pick a backend, or lock down the network.

## Using the sandbox

The model already has shell and file access through the default tools:

| Tool                       | Does                                |
| -------------------------- | ----------------------------------- |
| `bash`                     | run a shell command in the sandbox  |
| `read_file` / `write_file` | read/write files under `/workspace` |
| `glob`                     | find files by pattern               |
| `grep`                     | search file contents                |

All of them run with `/workspace` as the working directory. Any authored runtime function (a tool, a step, a model callback) can grab a live sandbox handle with `ctx.getSandbox()`.

```ts title="agent/tools/run_analysis.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";

export default defineTool({
  description: "Run a Python analysis script and return its output.",
  inputSchema: z.object({ script: z.string() }),
  async execute({ script }, ctx) {
    const sandbox = await ctx.getSandbox();
    await sandbox.writeTextFile({ path: "analysis/run.py", content: script });
    const result = await sandbox.run({ command: "python analysis/run.py" });
    return { stdout: result.stdout };
  },
});
```

`ctx.getSandbox()` takes no arguments, is async, and only works inside authored runtime execution.

`/workspace` is one namespace across every backend, so `/workspace/foo` points at the same file whether the backend is local or Vercel. When you need to interpolate a path into a generated command, `sandbox.resolvePath("repo/build.py")` anchors a relative path to its absolute `/workspace/repo/build.py` form. `readFile`, `writeFile`, and `run` do the same anchoring internally.

The handle does more than `run` and `writeTextFile`. In every method, relative paths resolve from `/workspace` and absolute paths pass through untouched:

| Method                                   | Does                                                                                            |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `run({ command })`                       | run one command, block until it exits, return `{ stdout, stderr, ... }`                         |
| `spawn(options)`                         | launch a long-running process (server, watcher) and return a `SandboxProcess` handle            |
| `readTextFile` / `writeTextFile`         | read/write a UTF-8 (or specified encoding) file; `readTextFile` supports 1-based line ranges    |
| `readBinaryFile` / `writeBinaryFile`     | read/write raw bytes (images, archives, anything non-text)                                      |
| `readFile` / `writeFile`                 | stream a file in/out as bytes                                                                   |
| `removePath({ path, force, recursive })` | delete one file or directory; `force` ignores missing paths, `recursive` removes non-empty dirs |
| `resolvePath(path)`                      | anchor a relative path to its absolute `/workspace/...` form                                    |
| `setNetworkPolicy(policy)`               | change egress policy mid-turn (Vercel backend only; see [Network policy](#network-policy))      |

Since `run` blocks until the command exits, reach for `spawn` when the process should keep running while the agent does other work:

```ts
const sandbox = await ctx.getSandbox();
const server = await sandbox.spawn({ command: "python -m http.server 8000" });
// ...do other work against the server...
await server.kill();
```

A `SandboxProcess` exposes `stdout`/`stderr` byte streams, `wait()` (resolves with the exit code), and `kill()` (idempotent).

`sandbox.id` is a stable per-session identifier that persists across reconnects to the same logical session. Use it as the cache key for per-session state that must outlive individual step executions.

The option types (`SandboxSpawnOptions`, `SandboxReadBinaryFileOptions`, `SandboxWriteBinaryFileOptions`, and so on) are named-exported from `eve/sandbox` alongside `SandboxProcess`.

## Seeding `/workspace`

Mount authored files into the sandbox at session start by placing them under `agent/sandbox/workspace/`. This requires the folder layout (`agent/sandbox/sandbox.ts`), not the top-level shorthand:

```text
agent/sandbox/
  sandbox.ts                ← optional override (see below)
  workspace/
    schema.sql              ← lands at /workspace/schema.sql
    scripts/run.sh          ← lands at /workspace/scripts/run.sh
```

Every file under `workspace/` mirrors into the sandbox cwd with its structure intact, and Eve lists the top-level entries to the model in the prompt automatically. One subtree is off limits: skill discovery already seeds skill files under `/workspace/skills/`, so authoring `agent/sandbox/workspace/skills/...` is rejected. Put those under `agent/skills/` instead.

## Overriding the sandbox

To add setup, seed files, or pick a backend, author `defineSandbox`. There are two layouts:

- `agent/sandbox.ts`: shorthand. Use it when you need only a definition, no seeded files.
- `agent/sandbox/sandbox.ts`: folder layout. Use it when you also seed `agent/sandbox/workspace/**`. If both exist, the folder layout wins.

```ts title="agent/sandbox/sandbox.ts"
import { defineSandbox, vercelBackend } from "eve/sandbox";

export default defineSandbox({
  backend: vercelBackend({ runtime: "node24", resources: { vcpus: 2 } }),
  revalidationKey: () => "repo-bootstrap-v1",
  async bootstrap({ use }) {
    const sandbox = await use();
    await sandbox.run({ command: "apt-get install -y jq" });
  },
  async onSession({ use }) {
    await use({ networkPolicy: "deny-all" });
  },
});
```

`defineSandbox` lives on `eve/sandbox`. Omit `backend` and the runtime falls back to `defaultBackend()` (see [Backends](#backends)).

## Backends

The backend decides where the sandbox runs. Eve ships three factories from `eve/sandbox`:

| Backend            | Runs the sandbox                                                                                 |
| ------------------ | ------------------------------------------------------------------------------------------------ |
| `vercelBackend()`  | on [Vercel Sandbox](https://vercel.com/docs/sandbox).                                            |
| `localBackend()`   | locally via `just-bash`. The default for `eve dev`.                                              |
| `defaultBackend()` | `vercelBackend()` on hosted Vercel (`process.env.VERCEL` set), `localBackend()` everywhere else. |

With `backend` omitted, Eve uses `defaultBackend()`: `eve dev` on your laptop boots the local backend, and a production build on Vercel uses Vercel Sandbox, no code change in between. `defaultBackend()` also accepts a keyed bag so each inner backend gets its own typed create options:

```ts
import { defaultBackend, defineSandbox } from "eve/sandbox";

export default defineSandbox({
  backend: defaultBackend({
    vercel: { networkPolicy: "deny-all", resources: { vcpus: 4 } },
    local: {},
  }),
});
```

In local development, `localBackend()` stores template and session files as normal directories under `.eve/sandbox-cache/local/`. Template directories are reused across sessions when the sandbox source, seed files, and `revalidationKey` still match; `eve dev` prunes stale local templates in the background. Session directories persist `/workspace` changes across turns for the same durable session.

You can write your own backend too: a `SandboxBackend` is just an object with a `name`, a `create`, and an optional `prewarm`. See the `SandboxBackend*` types on `eve/sandbox`.

## Lifecycle

There are two hooks, scoped differently:

- **`bootstrap({ use })`** is template-scoped and runs once when the template is built. Put reusable setup here, the kind every later session inherits: clone a baseline repo, install dependencies, seed files. Call `use()` to get a `SandboxSession`. Only template filesystem state and supported backend metadata carry into later sessions; config like network policy does not. If external inputs affect what bootstrap produces, set `revalidationKey: () => string` so Eve knows when to rebuild the template (authored sandbox source and seed contents are already tracked for you).
- **`onSession({ use, ctx })`** is durable-session-scoped and runs once per session. This is where per-session setup goes: network policy, resources, timeout, per-user credentials, one-time markers. Because it runs inside the active runtime context, it can read `ctx.session` and derive the current principal without baking credentials into the template. Call `use(opts?)` to get a `SandboxSession`; `opts` flow to the backend's update path after create.

```ts
import { defineSandbox, vercelBackend } from "eve/sandbox";

export default defineSandbox({
  backend: vercelBackend(),
  async onSession({ use, ctx }) {
    const sandbox = await use({ networkPolicy: "deny-all" });
    const user = ctx.session.auth.current;
    if (user === null) return;
    await sandbox.writeTextFile({ path: "SESSION_USER.txt", content: `${user.principalId}\n` });
  },
});
```

Sessions are persistent. When the VM shuts down (timeout, default 30 minutes), the filesystem is preserved and the sandbox resumes on the next message as if nothing happened, even days later.

## Network policy

Egress rules go on the backend factory or in `onSession`'s `use()`. There are three forms:

```ts
networkPolicy: "allow-all"; // default
networkPolicy: "deny-all";  // block all egress, including DNS

networkPolicy: {
  allow: ["ai-gateway.vercel.sh", "*.github.com"],
  subnets: { deny: ["10.0.0.0/8"] },
};
```

Set it on the factory (`vercelBackend({ networkPolicy: "deny-all" })`) and it applies from creation, including during `bootstrap`. Set it in `onSession`'s `use()` to override per-session. The common pattern combines both: leave the factory open so `bootstrap` can `git clone`, then lock down in `onSession`. And to change the policy mid-turn, call `sandbox.setNetworkPolicy(...)` on the live handle. That one is Vercel-only; the local backend rejects it.

## Credential brokering

Secrets never enter the sandbox. Instead, the network policy's per-domain `transform` injects credentials at the firewall, so a header can authenticate egress to a host while the secret stays out of the sandbox process entirely:

```ts
async onSession({ use }) {
  await use({
    networkPolicy: {
      allow: {
        "github.com": [{ transform: [{ headers: { authorization: "Basic …" } }] }],
        "*": [],
      },
    },
  });
}
```

The `"*": []` catch-all keeps general egress open while the `transform` applies only to `github.com`. For mid-turn brokering, call `setNetworkPolicy` with the same shape. The [Vercel Sandbox docs](https://vercel.com/docs/sandbox) cover the brokering mechanism itself.

## What to read next

- [Subagents](./subagents): each subagent gets its own sandbox, independent of its parent.
- [Tools](./tools): authored tools run in the app runtime (full `process.env`); only sandbox tools run in the sandbox.
- [Security model](./advanced/security-model): the app-runtime/sandbox trust boundary in full.
- [Vercel Sandbox](https://vercel.com/docs/sandbox): platform docs, including credential brokering and persistence limits.
