Skip to main content

Sandboxes and filesystems

A sandbox gives your agent an isolated environment where it can run commands, execute code, install dependencies, and manage processes. This lets agents perform work that would be risky, resource-intensive, or impractical to run directly inside your application.

Sandboxes are often temporary, so files created inside them may disappear when the environment stops. A filesystem gives the agent a place to read, write, and search files that can outlive the sandbox. You can use one to keep outputs between runs, seed a new sandbox with existing files, or give the agent documents it can search while working. Filesystems also work without a sandbox, for example when an agent only needs a knowledge base or access to files in a service such as Google Drive.

When to use sandboxes
Direct link to When to use sandboxes

Use a sandbox when an agent needs to:

  • Clone repositories and run shell, Git, build, or test workflows in a separate environment.
  • Process PDFs, presentations, or other files with specialized libraries and produce new artifacts.
  • Run long-lived or parallel work in separate environments with isolated files and processes.

If an agent only needs to read, write, or search files, configure direct filesystem access.

Quickstart
Direct link to Quickstart

Give an agent a local sandbox:

src/mastra/agents/coding-agent.ts
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 sandbox directory')
warning

LocalSandbox runs commands on the application host by default and isn't isolated or secure. Enable native isolation, or use a remote or container sandbox when running untrusted code.

A static sandbox is shared across every request and memory thread that uses the agent. Use a resolver when each user or thread needs a separate environment. See Lifecycle and persistence for sharing and cleanup details.

Using the sandbox
Direct link to Using the sandbox

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

ToolDoes
execute_commandRuns a shell command and returns stdout, stderr, and the exit code. Supports background: true when the backend can spawn long-running processes.
get_process_outputGets 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_processStops a background process and returns its recent output.

Configured capabilities determine which tools are available. Tool configuration can then disable, rename, or add policies to those tools:

import { LocalSandbox, Workspace, WORKSPACE_TOOLS } from '@mastra/core/workspace'

const workspace = new Workspace({
sandbox: new LocalSandbox({ workingDirectory: './workspace' }),
tools: {
requireApproval: true,
[WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: {
enabled: false,
},
},
})

Set { enabled: false } on one tool to remove it, or set the top-level enabled: false to disable generated tools by default. A per-tool { enabled: true } overrides that global setting. The top-level requireApproval policy applies to every generated tool unless a per-tool entry overrides it.

See the sandbox tools reference for all generated tools and the tool configuration reference for approvals, output limits, and hooks.

Authored runtime functions, including tools and workflow steps, can get the live sandbox from their execution context. Use it to execute commands, install dependencies, process files, or spawn a long-running process.

src/mastra/tools/start-server.ts
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) => {
if (!ctx.workspace || !ctx.requestContext) {
throw new Error('This tool requires a workspace execution context')
}

const sandbox = await ctx.workspace.resolveSandbox({
requestContext: ctx.requestContext,
})

if (!sandbox?.processes) {
throw new Error('The configured sandbox does not support background processes')
}

const server = await sandbox.processes.spawn(command)
return { pid: server.pid }
},
})

Sandboxes
Direct link to Sandboxes

LocalSandbox
Direct link to localsandbox

LocalSandbox 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.

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

Enable native isolation to restrict filesystem and network access at the operating-system level. On macOS, native isolation uses Seatbelt (sandbox-exec). On Linux, it uses Bubblewrap (bwrap). 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, write access to the working directory, and system binaries. You can also provide a custom Seatbelt profile or Bubblewrap arguments. See LocalSandbox for the full configuration.

Remote sandboxes
Direct link to Remote sandboxes

Use a remote or container sandbox when commands need a stronger boundary from the host application or when workloads need to scale beyond the resources of your application server. Each sandbox has its own isolation, persistence, networking, and mount behavior:

If Mastra doesn't support your sandbox provider, implement the sandbox provider interface to add it.

Filesystem
Direct link to Filesystem

Every sandbox has a filesystem for commands. LocalSandbox uses the host filesystem, while remote sandboxes have isolated filesystems that are often temporary. For files that need to outlive a sandbox, use external storage.

Mastra filesystems connect agents and sandboxes to provider-backed storage such as Amazon S3, Google Cloud Storage, or Google Drive. Configuring one gives the agent tools to read, write, and search files. When a remote sandbox and provider support mounting, Mastra automatically mounts the storage through Filesystem in Userspace (FUSE). Commands use normal file paths while the provider stores changes outside the sandbox.

See Filesystem for providers, file tools, and mounts.

Sandboxes per user or thread
Direct link to Sandboxes per user or thread

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 and caches one Daytona sandbox per user:

src/mastra/workspaces.ts
import { Workspace } from '@mastra/core/workspace'
import { DaytonaSandbox } from '@mastra/daytona'

const workspace = new Workspace({
sandbox: async ({ requestContext }) => {
const userId = requestContext.get('user-id') as string
const sandbox = new DaytonaSandbox({ id: `user-${userId}` })
await sandbox.start()
return sandbox
},
sandboxCacheKey: ({ requestContext }) => requestContext.get('user-id') as string,
})

The first request from a user creates the sandbox. Later requests with the same user ID reuse it.

For a sandbox with persistent storage, create a filesystem and mount it inside the resolver. This example creates one Daytona sandbox and one S3 storage prefix per memory thread, then mounts the storage at /workspace:

src/mastra/workspaces.ts
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) =>
requestContext.get(MASTRA_THREAD_ID_KEY) as string

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.

The example mounts the filesystem inside the resolver because static mounts can't be combined with a sandbox resolver. Generated tools resolve the filesystem and sandbox from the request context automatically.

warning

Your application owns sandboxes returned by a resolver. Destroy them and call workspace.clearSandboxCache(cacheKey) when the user, thread, or session ends. workspace.destroy() doesn't destroy resolver-returned sandboxes.

Tool availability
Direct link to Tool availability

With a static sandbox, Mastra knows which capabilities the backend supports and only gives the agent the corresponding tools. Application code should also check that an optional capability is available before using it.

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.

Background processes
Direct link to 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:

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 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, including programmatic process spawning, output, and standard input, see the SandboxProcessManager reference.

Lifecycle and persistence
Direct link to Lifecycle and persistence

Who shares a sandbox depends on where you configure it and whether you use a resolver:

ConfigurationWho shares the sandbox
Mastra-level configurationAgents that inherit the configuration from the Mastra instance use the same sandbox.
Agent-level configurationEvery request handled by that agent instance uses the same sandbox.
Resource-scoped resolverThe resolver caches one sandbox for each resource ID.
Thread-scoped resolverA 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 Sandboxes per user or thread.

Start
Direct link to Start

Static sandbox environments usually start when the first command runs, so most applications don't need to initialize them explicitly. Call workspace.init() during application startup only when you want to start a static sandbox and prepare its mounts before serving requests. This is useful when credential, network, or provider errors need to surface during startup instead of on the first command.

workspace.init() doesn't start resolver-backed sandboxes because no sandbox is selected until a request runs. The resolver must return a sandbox that's already started or can start itself on first use.

Hooks
Direct link to Hooks

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

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
Direct link to Cleanup

When you configure a sandbox directly instead of using a resolver, Mastra owns it. mastra dev and the generated Mastra server handle shutdown signals and destroy registered resources automatically. If you embed Mastra in a custom server or process, call mastra.shutdown() from its shutdown hook. For standalone use, call workspace.destroy() directly.

The backend decides what stop() and destroy() do. For example, stopping may shut down compute while preserving an environment that can restart, while destroying may delete the environment and its ephemeral files. Check the selected backend before relying on either behavior.

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

Persistence
Direct link to 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
Direct link to 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:

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:

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 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:

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