Skip to main content

Sandbox

Added in: @mastra/core@1.1.0

Sandbox providers give agents the ability to execute shell commands. When you configure a sandbox on a workspace, agents can run commands as part of their tasks.

A sandbox provider executes commands in a controlled environment:

  • Command execution: Run shell commands with arguments
  • Background processes: Spawn long-running processes like dev servers and watchers
  • Working directory: Commands run from a specific directory
  • Environment variables: Control what variables are available
  • Timeouts: Prevent long-running commands from hanging
  • Isolation: Optional OS-level sandboxing for security

Supported providers
Direct link to Supported providers

Basic usage
Direct link to Basic usage

Create a workspace with a sandbox and assign it to an agent. The agent can then execute shell commands:

src/mastra/agents/dev-agent.ts
import { Agent } from '@mastra/core/agent'
import { Workspace, LocalFilesystem, LocalSandbox } from '@mastra/core/workspace'

const workspace = new Workspace({
filesystem: new LocalFilesystem({
basePath: './workspace',
}),
sandbox: new LocalSandbox({
workingDirectory: './workspace',
}),
})

const agent = new Agent({
id: 'dev-agent',
model: 'openai/gpt-5.5',
instructions: 'You are a helpful development assistant.',
workspace,
})

// The agent now has the execute_command tool available
const response = await agent.generate('Run `ls -la` in the workspace directory')

See LocalSandbox reference for configuration options including environment isolation and native OS sandboxing.

Dynamic sandbox
Direct link to Dynamic sandbox

The sandbox option accepts a resolver function instead of a static instance. The resolver receives requestContext and returns a sandbox per request, allowing a single workspace to serve different sandboxes based on the caller's identity, role, or tenant.

src/mastra/workspaces.ts
import { Agent } from '@mastra/core/agent'
import { Workspace, LocalSandbox } from '@mastra/core/workspace'

const workspace = new Workspace({
sandbox: ({ requestContext }) => {
const userId = requestContext.get('user-id') as string
return new LocalSandbox({
workingDirectory: `/workspaces/${userId}`,
})
},
})

const agent = new Agent({
id: 'multi-tenant-agent',
model: 'your-provider/your-model',
workspace,
})

Each request resolves its own sandbox at tool execution time:

import { RequestContext } from '@mastra/core/request-context'

// User Alice — commands run in /workspaces/alice
const aliceCtx = new RequestContext([['user-id', 'alice']])
await agent.generate('List files in cwd', { requestContext: aliceCtx })

// User Bob — commands run in /workspaces/bob
const bobCtx = new RequestContext([['user-id', 'bob']])
await agent.generate('List files in cwd', { requestContext: bobCtx })

By default, workspace instructions describe the dynamic sandbox with stable placeholder text. See Workspace instructions to include concrete per-request details.

The resolver can also be asynchronous — for example to look up tenant configuration from a database:

const workspace = new Workspace({
sandbox: async ({ requestContext }) => {
const tenant = await db.getTenant(requestContext.get('tenant-id'))
return new LocalSandbox({ workingDirectory: tenant.workspacePath })
},
})

Lifecycle ownership
Direct link to Lifecycle ownership

When the sandbox is a static instance, workspace.init() calls its start() method and workspace.destroy() calls its destroy() method. With a resolver, the workspace has no instance to manage at construction time — the caller owns the returned sandbox's lifecycle.

The resolver must return a sandbox that's ready to use, either already started or able to handle calls without explicit startup. The caller also owns cleanup timing for returned sandboxes. Cleanup might happen per request, per tenant, per user, or as part of a long-lived sandbox pool; workspace.destroy() doesn't destroy resolver-returned sandboxes.

note

sandbox resolvers are incompatible with mounts and lsp: true. Both require a concrete sandbox instance at construction time, so combining them with a resolver throws an INVALID_CONFIG error (for mounts) or disables LSP with a warning (for lsp: true).

Tool registration
Direct link to Tool registration

With a static sandbox, the workspace inspects the instance to decide which tools to register. With a resolver, the workspace assumes full capabilities and registers execute_command (with background support), get_process_output, and kill_process. If the resolved sandbox doesn't implement a capability, the runtime throws a clear SandboxFeatureNotSupportedError.

Background process continuity
Direct link to Background process continuity

Background processes can outlive a single tool call, so get_process_output and kill_process must reach the same sandbox that started the process. By default, a resolved sandbox is cached per request. For continuity across follow-up requests, such as a later conversation turn, set sandboxCacheKey to a stable identifier. The resolved sandbox is then cached by that key instead of by request:

const workspace = new Workspace({
sandbox: ({ requestContext }) => resolveSandbox(requestContext),
sandboxCacheKey: ({ requestContext }) => requestContext.get('thread-id') as string,
})

Without a sandboxCacheKey, the resolver must return the same sandbox itself for follow-up calls that share a tenant, user, or session.

When a cached sandbox is no longer needed, destroy the sandbox in your own lifecycle code and call workspace.clearSandboxCache(cacheKey) to drop the workspace cache entry. Call workspace.clearSandboxCache() to clear all keyed sandbox entries.

Workspace instructions
Direct link to Workspace instructions

Workspace instructions describe the environment in the agent's system message. With a sandbox resolver, the workspace doesn't call the resolver to build these instructions. It emits stable placeholder text, so constructing the prompt never provisions a caller-owned sandbox and the system message stays consistent across requests, which keeps prompt caching effective.

To include concrete per-request sandbox details, set instructions.dynamicSandbox to 'resolve':

const workspace = new Workspace({
sandbox: ({ requestContext }) => resolveSandbox(requestContext),
instructions: { dynamicSandbox: 'resolve' },
})

'resolve' calls the resolver on every request, which may provision the sandbox and makes the system message request-specific. Pass a function instead to return custom text from requestContext without resolving the sandbox:

const workspace = new Workspace({
sandbox: ({ requestContext }) => resolveSandbox(requestContext),
instructions: {
dynamicSandbox: ({ requestContext }) =>
`Sandbox scoped to tenant ${requestContext.get('tenant-id')}.`,
},
})

Agent tools
Direct link to Agent tools

When you configure a sandbox on a workspace, agents receive the execute_command tool for running shell commands.

If your sandbox provider supports running processes in the background, the execute_command tool also accepts background: true for starting long-running processes, and two additional tools are registered:

ToolDescription
execute_commandRun a shell command. Returns stdout, stderr, and exit code. Supports background: true to spawn a long-running process and return a PID.
get_process_outputGet stdout, stderr, and status of a background process by PID. Supports tail to limit output lines and wait: true to block until exit.
kill_processStop a background process by PID. Returns recent output.

These tools are registered automatically. See Workspace class reference for the full tool name list.

Background process callbacks
Direct link to Background process callbacks

When agents start background processes through the execute_command tool, you can receive lifecycle callbacks for stdout, stderr, and process exit. Configure these through the backgroundProcesses option on the execute_command tool:

src/mastra/workspaces.ts
import { Workspace, LocalSandbox, WORKSPACE_TOOLS } from '@mastra/core/workspace'

const workspace = new Workspace({
sandbox: new LocalSandbox({ workingDirectory: './workspace' }),
tools: {
[WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: {
backgroundProcesses: {
onStdout: (data, { pid }) => console.log(`[${pid}] ${data}`),
onStderr: (data, { pid }) => console.error(`[${pid}] ${data}`),
onExit: ({ pid, exitCode }) => console.log(`Process ${pid} exited: ${exitCode}`),
},
},
},
})

These callbacks fire for all background processes started by the agent through the execute_command tool.

Abort signal
Direct link to Abort signal

By default, background processes inherit the agent's abort signal and are killed when the agent disconnects. Control this behavior with the abortSignal option:

  • undefined (default): Uses the agent's abort signal
  • AbortSignal: Uses a custom signal
  • null or false: Disables abort — processes persist after agent shutdown
src/mastra/workspaces.ts
import { Workspace, LocalSandbox, WORKSPACE_TOOLS } from '@mastra/core/workspace'

const workspace = new Workspace({
sandbox: new LocalSandbox({ workingDirectory: './workspace' }),
tools: {
[WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: {
backgroundProcesses: {
abortSignal: null, // Processes survive agent disconnection
},
},
},
})

Use null or false for cloud sandboxes (for example, E2B, Daytona, or Modal) where processes should outlive the agent.

note

For the full SandboxProcessManager API (spawning processes programmatically, reading output, sending stdin), see the SandboxProcessManager reference.