> Discover all available pages from the documentation index: https://mastra.ai/llms.txt

# Sandbox

Sandboxes give Mastra agents an environment for running commands and working with files. Agents can also start background processes when the selected backend supports process management; otherwise, background-process tools are unavailable.

In Mastra, you attach a sandbox backend to a `Workspace`. You can use one sandbox for an application or agent, or resolve separate environments for each user, tenant, or thread.

Supported backends can FUSE-mount filesystems into the sandbox, letting you seed ephemeral environments with files and persist their output between runs.

## When to use sandboxes?

Sandboxes are useful for:

- **Coding agents and software factories**: Clone repositories and run shell, Git, build, or test workflows without giving autonomous agents access to the host system.
- **Deep research and analysis**: Use specialized libraries to process downloaded PDFs or presentations and produce new artifacts.
- **Long-running and parallel tasks**: Run work in separate environments without tying it to one request or sharing files and processes.

## Quickstart

In Mastra, a sandbox operates through a `Workspace`. Its `sandbox` backend defines where commands run.

Create an agent with a workspace and sandbox:

```typescript
import { Agent } from '@mastra/core/agent'
import { LocalSandbox, Workspace } from '@mastra/core/workspace'

export const codingAgent = new Agent({
  id: 'coding-agent',
  name: 'Coding agent',
  instructions: 'Use the sandbox to complete coding tasks.',
  model: 'openai/gpt-5.6-sol',
  workspace: new Workspace({
    sandbox: new LocalSandbox({
      workingDirectory: './workspace',
    }),
  }),
})

await codingAgent.generate('List the files in the workspace directory')
```

> **Warning:** `LocalSandbox` is the quickest sandbox to set up, but commands run on the host by default. Enable [native isolation](#localsandbox) whenever possible. For applications exposed to untrusted users, use a remote or container backend with a stronger isolation boundary instead.

To use a different sandbox for each user, tenant, or thread, configure a [sandbox resolver](#multi-tenant-sandboxes) instead.

## Using the sandbox

Agents receive tools for the capabilities supported by the sandbox backend:

| Tool                 | Does                                                                                                                                                                                                                                    |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `execute_command`    | Runs a shell command and returns stdout, stderr, and the exit code. Supports `background: true` when the backend can spawn long-running processes.                                                                                      |
| `get_process_output` | Gets stdout, stderr, and status for a background process. Supports `tail` to limit output and `wait: true` to wait for the process to exit. This tool and `kill_process` are only added when the backend supports background processes. |
| `kill_process`       | Stops a background process and returns its recent output.                                                                                                                                                                               |

You can disable or rename each tool and require approval before it runs. You can also limit how much output it adds to the model's context. See the [workspace tool configuration reference](https://mastra.ai/reference/workspace/workspace-class).

Authored runtime functions, including tools and workflow steps, can get the live sandbox from their execution context. Start a server, watcher, notebook kernel, browser, or worker and interact with it across agent turns.

```typescript
import type { ProcessHandle } from '@mastra/core/sandbox'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const startServerTool = createTool({
  id: 'start-server',
  description: 'Starts a server in the sandbox',
  inputSchema: z.object({
    command: z.string(),
  }),
  execute: async ({ command }, ctx) => {
    const sandbox = await ctx.getSandbox()
    const server: ProcessHandle = await sandbox.spawn({ command })
    return { pid: server.pid }
  },
})
```

The authored function itself runs in your application process. Only operations called through the sandbox handle run inside the sandbox.

## Supported backends

### `LocalSandbox`

[`LocalSandbox`](https://mastra.ai/reference/workspace/local-sandbox) executes commands on the same machine as your Mastra application. By default, commands run directly on the host with the permissions of the application process.

Enable native isolation to restrict filesystem and network access at the operating-system level:

- **macOS**: Seatbelt (`sandbox-exec`)
- **Linux**: Bubblewrap (`bwrap`)

```typescript
const sandbox = new LocalSandbox({
  workingDirectory: './workspace',
  isolation: 'seatbelt', // Use 'bwrap' on Linux
  nativeSandbox: {
    allowNetwork: false,
    readOnlyPaths: ['./reference-data'],
  },
})
```

Use `LocalSandbox.detectIsolation()` to check whether Seatbelt or Bubblewrap is available on the current operating system. The `nativeSandbox` options control network access, read-only or writable paths, workspace write access, and system binaries. You can also provide a custom Seatbelt profile or Bubblewrap arguments. See [`LocalSandbox`](https://mastra.ai/reference/workspace/local-sandbox) for the full configuration.

### Other backends

Use a remote or container backend when commands need a stronger boundary from the host application. Each backend has its own isolation, persistence, networking, and mount behavior:

- [AgentCore](https://mastra.ai/integrations/sandboxes/agentcore)
- [Apple Container](https://mastra.ai/integrations/sandboxes/apple-container)
- [Blaxel](https://mastra.ai/integrations/sandboxes/blaxel)
- [Daytona](https://mastra.ai/integrations/sandboxes/daytona)
- [Docker](https://mastra.ai/integrations/sandboxes/docker)
- [E2B](https://mastra.ai/integrations/sandboxes/e2b)
- [Mastra](https://mastra.ai/reference/workspace/platform-sandbox)
- [Modal](https://mastra.ai/integrations/sandboxes/modal)
- [Railway](https://mastra.ai/integrations/sandboxes/railway)
- [Vercel](https://mastra.ai/integrations/sandboxes/vercel)

If Mastra doesn't support your execution backend, implement [`WorkspaceSandbox`](https://mastra.ai/reference/workspace/sandbox) to add it.

## Persistent storage

Some execution backends can also mount cloud filesystems through FUSE. Mounted files appear as local directories inside the sandbox, so commands such as `cat /data/report.md` or `python /data/analyze.py` can use them directly.

Configure mounts on the workspace alongside the sandbox:

```typescript
import { Workspace } from '@mastra/core/workspace'
import { DaytonaSandbox } from '@mastra/daytona'
import { S3Filesystem } from '@mastra/s3'

const workspace = new Workspace({
  mounts: {
    '/data': new S3Filesystem({
      bucket: 'agent-data',
      region: 'us-east-1',
    }),
  },
  sandbox: new DaytonaSandbox(),
})
```

The agent receives filesystem tools for the mounted storage, and commands in the sandbox can access the same files under `/data`. This lets you seed an ephemeral sandbox with existing files and persist its output after the sandbox stops.

Mount support varies by sandbox backend. See [Filesystem](https://mastra.ai/docs/sandbox/filesystem) for supported storage backends, detailed mount configuration, and how to mount multiple filesystems.

## Multi-tenant sandboxes

Use a **resolver** when each user, tenant, or thread needs a separate sandbox. Set `sandboxCacheKey` to the identity that owns the sandbox so later requests reuse the same live environment.

This example creates one Daytona sandbox and one S3 storage prefix per memory thread. The S3 filesystem is also mounted at `/workspace` inside the sandbox:

```typescript
import { MASTRA_THREAD_ID_KEY, type RequestContext } from '@mastra/core/request-context'
import { Workspace } from '@mastra/core/workspace'
import { DaytonaSandbox } from '@mastra/daytona'
import { S3Filesystem } from '@mastra/s3'

const getThreadId = (requestContext: RequestContext) => {
  const threadId = requestContext.get(MASTRA_THREAD_ID_KEY)
  if (typeof threadId !== 'string' || !threadId) {
    throw new Error('A memory thread is required to use this workspace')
  }

  return threadId
}

const createThreadFilesystem = (threadId: string) =>
  new S3Filesystem({
    bucket: process.env.S3_BUCKET!,
    region: process.env.S3_REGION!,
    prefix: `threads/${threadId}`,
  })

const workspace = new Workspace({
  filesystem: ({ requestContext }) => createThreadFilesystem(getThreadId(requestContext)),
  sandbox: async ({ requestContext }) => {
    const threadId = getThreadId(requestContext)
    const sandbox = new DaytonaSandbox({
      id: `thread-${threadId}`,
      language: 'typescript',
    })

    await sandbox.start()
    await sandbox.mount(createThreadFilesystem(threadId), '/workspace')
    return sandbox
  },
  sandboxCacheKey: ({ requestContext }) => getThreadId(requestContext),
})
```

The first request in a thread runs the resolver and starts the sandbox. Later requests with the same thread ID reuse the cached sandbox. A different thread ID creates a different sandbox and storage prefix.

Workspace-level `mounts` can't be combined with a sandbox resolver. This is a known limitation, so the example mounts the filesystem inside the resolver instead. Workspace tools resolve the filesystem and sandbox from the request context automatically.

### Resolver ownership

The workspace doesn't own sandboxes returned by a resolver. Return a sandbox that's ready to use, and destroy it through your application's lifecycle code when it's no longer needed. `workspace.destroy()` doesn't destroy resolver-returned sandboxes.

Resolvers are incompatible with `mounts` and [`lsp: true`](https://mastra.ai/docs/sandbox/lsp), because both require a static sandbox when the workspace is constructed. Using a resolver with `mounts` throws an `INVALID_CONFIG` error. With `lsp: true`, Mastra disables LSP and logs a warning.

### Tool availability

With a static sandbox, Mastra knows which capabilities the backend supports and only gives the agent the corresponding tools. With a resolver-backed sandbox, the backend isn't known until a request runs, so Mastra initially makes all sandbox tools available. If the resolved backend doesn't support the tool the agent calls, the call fails with `SandboxFeatureNotSupportedError`.

For example, this resolver returns a Daytona sandbox for development requests and an AgentCore sandbox for other requests:

```typescript
const workspace = new Workspace({
  sandbox: ({ requestContext }) =>
    requestContext.get('environment') === 'development'
      ? new DaytonaSandbox()
      : new AgentCoreRuntimeSandbox({
          agentRuntimeArn: process.env.AGENTCORE_RUNTIME_ARN!,
        }),
})
```

Mastra exposes the process tools because `DaytonaSandbox` supports background processes. If a request resolves to `AgentCoreRuntimeSandbox`, one-shot commands still work, but calling `get_process_output` or `kill_process` fails with `SandboxFeatureNotSupportedError` because that backend doesn't support background processes.

## Background processes

When agents start background processes through `execute_command`, you can receive lifecycle callbacks for stdout, stderr, and process exit. Configure these through the `backgroundProcesses` option on its `WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND` entry:

```typescript
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 `execute_command`.

By default, background processes inherit the agent's abort signal and stop when the agent disconnects. Set `abortSignal` to a custom signal, or use `null` or `false` when the process should continue after the request ends.

For the full `SandboxProcessManager` API (spawning processes programmatically and reading output, plus sending stdin), see the [`SandboxProcessManager` reference](https://mastra.ai/reference/workspace/process-manager).

## Lifecycle and persistence

Sandbox scope depends on where you assign the workspace and whether you use a resolver:

| Configuration            | Sandbox scope                                                                    |
| ------------------------ | -------------------------------------------------------------------------------- |
| Mastra-level workspace   | Agents that inherit the workspace from the Mastra instance use the same sandbox. |
| Agent-level workspace    | Every request handled by that agent instance uses the same sandbox.              |
| Resource-scoped resolver | The resolver caches one sandbox for each resource ID.                            |
| Thread-scoped resolver   | A memory thread keeps its sandbox across requests in that thread.                |

A static sandbox isn't automatically scoped to the current resource or memory thread. For resource or thread scope, use a resolver and set `sandboxCacheKey` to the corresponding ID. See [Multi-tenant sandboxes](#multi-tenant-sandboxes).

### Start

Static sandbox backends are instantiated with your application, but their execution environment usually starts lazily on the first command. Call `workspace.init()` when you want to provision it during application startup instead.

Resolver-backed sandboxes aren't started by `workspace.init()` because no backend is selected until the resolver runs. Return a sandbox that's already started or can start itself on first use.

### Hooks

Use `onStart`, `onStop`, and `onDestroy` to run application code during lifecycle transitions. Hooks receive the live sandbox instance:

```typescript
const sandbox = new LocalSandbox({
  workingDirectory: './workspace',
  onStart: ({ sandbox }) => console.log(`Started ${sandbox.id}`),
  onStop: ({ sandbox }) => console.log(`Stopping ${sandbox.id}`),
  onDestroy: ({ sandbox }) => console.log(`Destroying ${sandbox.id}`),
})
```

`onStart` runs after the sandbox starts. `onStop` and `onDestroy` run before their corresponding operation.

### Cleanup

Sandboxes passed directly to a workspace are owned by that workspace. `mastra dev` and the generated Mastra server handle shutdown signals and destroy registered workspaces automatically. If you embed Mastra in a custom server or process, call `mastra.shutdown()` from its shutdown hook. For a standalone workspace, call `workspace.destroy()` directly. The effect of stopping or destroying the underlying environment depends on the backend.

Sandboxes returned by a resolver are owned by your application. `workspace.destroy()` and `mastra.shutdown()` clear workspace references but don't destroy those resolved sandboxes. Your resolver or application lifecycle must keep track of them, call `destroy()` when their user, thread, or session ends, and then call `workspace.clearSandboxCache(cacheKey)` for keyed entries. This prevents unused compute from continuing to run and later requests from reusing a stale sandbox.

### Persistence

Persistence is backend-specific. Files and processes aren't guaranteed to survive when a sandbox stops or the application restarts. An idle timeout may also discard them. Some backends reconnect by sandbox ID or preserve snapshots and volumes. Others create a fresh environment.

Every sandbox has an `id`, but the ID isn't a cross-backend persistence guarantee. Read the selected backend's reference before relying on reconnection or persisted state.

## Network and credential safety

Mastra doesn't define one network policy that applies to every sandbox backend. Defaults and supported controls vary, so check the selected backend before running untrusted code. For sensitive or production workloads, prefer blocking outbound access or allowing only the destinations the task needs. Apply that policy when you create the sandbox so it takes effect before commands run.

If an agent starts a web server inside a remote sandbox, that server initially listens on a port inside the sandbox. Backends that support port exposure implement `sandbox.networking.getPortUrl()` to return a URL you can use to preview or test the server from outside the sandbox:

```typescript
const url = await sandbox.networking?.getPortUrl(8000)
```

Depending on the backend's access controls, the URL may make the service reachable to others. This inbound exposure is separate from outbound sandbox access. `LocalSandbox` with native isolation blocks network access by default unless `allowNetwork` is enabled. Without native isolation, local commands use the host network.

For example, `DaytonaSandbox` allows outbound access unless you configure a restriction. Set `networkBlockAll` with a domain allowlist when the task only needs specific services:

```typescript
import { DaytonaSandbox } from '@mastra/daytona'

const sandbox = new DaytonaSandbox({
  networkBlockAll: true,
  domainAllowList: 'registry.npmjs.org,api.github.com',
})
```

Daytona applies these settings when it creates the sandbox. It also supports CIDR rules through `networkAllowList`. See the [Daytona integration page](https://mastra.ai/integrations/sandboxes/daytona) for full configuration details.

Environment variables passed to a sandbox or command are available to code running there. Mastra doesn't provide a backend-neutral credential broker that keeps secrets outside the sandbox while authorizing its requests. Give the command only the credentials it needs, scoped to the task. Follow the selected backend's secret-management guidance.

For example, pass a read-only token to the command that needs it instead of configuring it for every command in the sandbox:

```typescript
await sandbox.executeCommand('node', ['scripts/download-reports.js'], {
  env: {
    REPORTS_READ_TOKEN: process.env.REPORTS_READ_TOKEN,
  },
})
```

## Related

- [`SandboxProcessManager` reference](https://mastra.ai/reference/workspace/process-manager)
- [`WorkspaceSandbox` reference](https://mastra.ai/reference/workspace/sandbox)
- [Filesystem](https://mastra.ai/docs/sandbox/filesystem)