Sandbox

Give your agents a real workspace to run code, edit files, and preview apps.

Sandbox gives your agents a real workspace.

With it, agents can:

  • run shell commands
  • read and write files
  • create folders and remove paths
  • expose a local port as a preview URL
  • create and delete sandboxes as it works

Built-in clients are available for:

  • E2B
  • Daytona

Want to bring your own sandbox runtime? See Custom Sandbox Clients below.

This is a good fit for coding agents, debugging agents, and any workflow that needs a real execution environment.

The built-in E2B and Daytona clients load their SDKs dynamically. If you use one, install that SDK in your app first.

bun add e2b
bun add @daytonaio/sdk

Quick Start With E2B

import { betterAgent } from "@better-agent/core";
import { sandboxPlugin, createE2BSandboxClient } from "@better-agent/plugins";

export const app = betterAgent({
  agents: [myAgent],
  plugins: [
    sandboxPlugin({
      client: createE2BSandboxClient({
        apiKey: process.env.E2B_API_KEY,
      }),
    }),
  ],
});

This gives your agents a real workspace backed by E2B.

Quick Start With Daytona

import { betterAgent } from "@better-agent/core";
import {
  sandboxPlugin,
  createDaytonaSandboxClient,
} from "@better-agent/plugins";

export const app = betterAgent({
  agents: [myAgent],
  plugins: [
    sandboxPlugin({
      client: createDaytonaSandboxClient({
        apiKey: process.env.DAYTONA_API_KEY,
        target: process.env.DAYTONA_TARGET,
        template: "node:20",
        templateKind: "image",
      }),
    }),
  ],
});

This gives your agents a real workspace backed by Daytona.

What Tools It Adds

By default, the plugin creates these tools:

  • sandbox_exec
  • sandbox_read_file
  • sandbox_write_file
  • sandbox_list_files
  • sandbox_make_dir
  • sandbox_remove_path
  • sandbox_get_host
  • sandbox_kill

If you want a different naming scheme, use prefix.

const plugin = sandboxPlugin({
  prefix: "workspace",
  client: createE2BSandboxClient({
    apiKey: process.env.E2B_API_KEY,
  }),
});

This creates tools like workspace_exec and workspace_write_file.

Managed Sandbox Reuse

By default, the plugin reuses one sandbox per conversation.

That means if a conversation starts a sandbox, later sandbox tool calls in the same conversation keep using it automatically.

If there is no conversationId, the plugin does not automatically reuse a sandbox.

You can also take full control with sessionKey.

const plugin = sandboxPlugin({
  client: createE2BSandboxClient({
    apiKey: process.env.E2B_API_KEY,
  }),
  sessionKey: ({ agentName, conversationId }) => {
    if (!conversationId) return undefined;
    return `${agentName}:${conversationId}`;
  },
});

Rules:

  • return a non-empty string to reuse a sandbox under that key
  • return null or undefined to disable reuse for that call

When the plugin kills the managed sandbox for a session, it also clears the stored mapping for that session.

Targeting An Existing Sandbox

If you already have a sandbox id, tell the agent to use it in the tool call.

await app.run("coder", {
  input:
    'Use the existing sandbox "sbx_123" to inspect the project files and check the current workspace state.',
});

In that tool call, sandboxId tells the plugin to use the existing sandbox instead of looking up the managed sandbox for the current conversation.

Useful Defaults

Use defaults when you want every auto-created sandbox to start the same way.

const plugin = sandboxPlugin({
  client: createDaytonaSandboxClient({
    apiKey: process.env.DAYTONA_API_KEY,
  }),
  defaults: {
    template: "base-dev-env",
    timeoutMs: 10 * 60_000,
    envs: {
      NODE_ENV: "development",
    },
  },
});

This is useful when your agents always need the same template, environment variables, or timeout settings.

Approval Controls

Sandbox tools can run commands and modify files, so you may want approval gates for the riskier actions.

const plugin = sandboxPlugin({
  client: createE2BSandboxClient({
    apiKey: process.env.E2B_API_KEY,
  }),
  approvals: {
    exec: { required: true },
    writeFile: { required: true },
    removePath: { required: true },
    killSandbox: { required: true },
  },
});

Shared Storage

The default store is in-memory, which is fine for local development or a single process.

If you need sandbox reuse across multiple workers or instances, provide a custom store with:

  • get(key)
  • set(key, sandboxId)
  • delete(key)

Preview URLs

Use sandbox_get_host when your agent starts a server inside the sandbox and needs a public preview URL.

Providers may return:

  • a plain URL string
  • a URL plus a token when preview access needs authentication

Advanced

Custom Sandbox Clients

You are not limited to the built-in E2B and Daytona clients.

If you already have your own sandbox runtime or internal platform, you can pass a custom client to sandboxPlugin as long as it implements the SandboxClient interface.

import type { SandboxClient } from "@better-agent/plugins";
import { sandboxPlugin } from "@better-agent/plugins";

const client: SandboxClient = {
  async createSandbox() {
    return { sandboxId: "sbx_custom_123" };
  },
  async runCommand({ sandboxId, cmd }) {
    return mySandboxRuntime.exec(sandboxId, cmd);
  },
  async readFile({ sandboxId, path }) {
    return mySandboxRuntime.readFile(sandboxId, path);
  },
  async writeFile({ sandboxId, path, content }) {
    await mySandboxRuntime.writeFile(sandboxId, path, content);
    return { path };
  },
  async listFiles({ sandboxId, path }) {
    return mySandboxRuntime.listFiles(sandboxId, path);
  },
  async makeDir({ sandboxId, path }) {
    await mySandboxRuntime.makeDir(sandboxId, path);
    return { created: true };
  },
  async removePath({ sandboxId, path }) {
    await mySandboxRuntime.removePath(sandboxId, path);
  },
  async getHost({ sandboxId, port }) {
    return mySandboxRuntime.getHost(sandboxId, port);
  },
  async killSandbox({ sandboxId }) {
    await mySandboxRuntime.kill(sandboxId);
  },
};

const plugin = sandboxPlugin({ client });