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/sdkQuick 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_execsandbox_read_filesandbox_write_filesandbox_list_filessandbox_make_dirsandbox_remove_pathsandbox_get_hostsandbox_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
nullorundefinedto 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 });