Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Sandbox

Execute real sandbox commands across Docker, Apple Container, Microsandbox, Daytona, virtual, and custom backends

The sandbox system lets AI agents execute real commands in isolated environments. Choose between Docker containers for portable full binary execution, Apple Container lightweight VMs for stronger local isolation on Apple silicon, Microsandbox microVMs for local hardware-isolated OCI images, caller-owned Daytona sandboxes for managed remote workspaces, virtual file systems for simulation, custom Sandbox implementations for specialized policy, or binary bridges to combine approaches.

When to Use What

ApproachUse CaseRequires Docker?
createDockerSandbox + createBashToolAgent needs real binaries in isolated containerYes
createDockerSandboxDirect container control without AI agent wiringYes
createAppleContainerSandbox + createBashToolAgent needs real binaries in Apple Container lightweight VMsNo
createAppleContainerSandboxDirect VM-backed container control on Apple silicon/macOS 26+No
createMicrosandboxSandbox + createBashToolAgent needs OCI images in local microVMs with streaming process outputNo
createMicrosandboxSandboxDirect local microVM control with get-or-create named sandboxesNo
createDaytonaSandbox(client, options) + createBashToolManaged remote sandbox with caller-owned Daytona clientNo
createVirtualSandboxIn-process bash with a virtual filesystem and optional JavaScript command supportNo
createBinaryBridgesBridge specific host binaries into just-bash virtual FSNo

All AI-agent-facing surfaces compose the same way: pick a backend factory (createDockerSandbox / createAppleContainerSandbox / createMicrosandboxSandbox / createDaytonaSandbox / createVirtualSandbox), then wrap it with createBashTool({ sandbox }) to get the bash tool and file tools. Skill provisioning and discovery remain the application's responsibility. For per-command file-change tracking, first decorate a real backend with withStraceFileChanges(...), then pass that tracked backend into createBashTool.

uploadDirectory accepts one source or an array of sources. Each source may set its own sandbox destination; otherwise it uploads under the Bash working directory.

All backends expose the buffered command + file API (executeCommand, readFile, writeFiles, dispose). Backends that can stream real processes, including Docker, Apple Container, Microsandbox, and Daytona sandboxes, additionally expose an optional spawn(command, options?) for live streaming stdio. Feature-detect it with if (!sandbox.spawn) { ... } when writing backend-agnostic code.

executeCommand and spawn execute commands with Bash semantics. Process-backed environments must contain bash; factories fail with a clear error when Bash cannot be started. There is no POSIX sh fallback.

Every backend is also AsyncDisposable: it implements [Symbol.asyncDispose] (which delegates to dispose()), so you can bind it with await using and let the sandbox release at scope exit instead of writing try/finally:

await using backend = await createDockerSandbox({ installers: [pkg(['jq'])] });
await backend.executeCommand('jq --version');
// backend.dispose() runs automatically when this scope exits

Quick Start: Docker + Bash Tool

The fastest path for giving an AI agent a bash tool that runs in Docker:

import { groq } from '@ai-sdk/groq';
import { printer } from '@deepagents/agent';
import {
  type AvailableSkill,
  ContextEngine,
  InMemoryContextStore,
  agent,
  createBashTool,
  createDockerSandbox,
  pkg,
  role,
  skills,
  user,
} from '@deepagents/context';

const backend = await createDockerSandbox({
  installers: [pkg(['curl', 'jq'])],
});
const sandbox = await createBashTool({
  sandbox: backend,
  uploadDirectory: {
    source: '/opt/agent-skills/deploy',
    destination: '/skills/deploy',
  },
});

// The application owns the source and builds this catalog from its own
// manifest, database, or discovery logic.
const availableSkills: AvailableSkill[] = [
  {
    name: 'deploy',
    description: 'Deploy services safely.',
    path: '/skills/deploy/SKILL.md',
  },
];

try {
  const context = new ContextEngine({
    chatId: 'demo',
    userId: 'user-1',
    store: new InMemoryContextStore(),
  });

  context.set(
    role('You are a helpful assistant with bash access.'),
    skills(availableSkills),
    user('Use the most relevant skill to help me.'),
  );

  const assistant = agent({
    name: 'Assistant',
    sandbox,
    model: groq('gpt-oss-20b'),
    context,
  });

  await printer.stdout(await assistant.stream());
} finally {
  await sandbox.sandbox.dispose();
}

Quick Start: Docker Sandbox

For direct container control without AI agent wiring:

import { createDockerSandbox, pkg } from '@deepagents/context';

const sandbox = await createDockerSandbox({
  image: 'bash:5.3-alpine3.24',
  installers: [pkg(['curl', 'jq'])],
  volumes: [
    {
      type: 'bind',
      hostPath: process.cwd(),
      containerPath: '/workspace',
      readOnly: true,
    },
  ],
  resources: { memory: '512m', cpus: 1 },
});

try {
  const result = await sandbox.executeCommand('curl --version');
  console.log(result.stdout);

  await sandbox.writeFiles([
    { path: '/tmp/hello.txt', content: 'Hello from Docker!' },
  ]);

  const content = await sandbox.readFile('/tmp/hello.txt');
  console.log(content);
} finally {
  await sandbox.dispose();
}

Or use useSandbox for automatic cleanup:

import { useSandbox } from '@deepagents/context';

const output = await useSandbox(
  { installers: [pkg(['curl'])] },
  async (sandbox) => {
    const result = await sandbox.executeCommand('curl --version');
    return result.stdout;
  },
);

Quick Start: Binary Bridges

Bridge specific host binaries into a just-bash virtual file system (no Docker needed):

import { Bash, ReadWriteFs } from 'just-bash';
import { createBashTool, createBinaryBridges } from '@deepagents/context';

const bridges = createBinaryBridges(
  'node',
  { name: 'python', binaryPath: 'python3' },
  { name: 'git', allowedArgs: /^(status|log|diff|show)/ },
);

const { bash } = await createBashTool({
  sandbox: new Bash({
    fs: new ReadWriteFs({ root: process.cwd() }),
    customCommands: bridges,
  }),
});

Binary bridges resolve virtual paths to real host paths and use the host's PATH for binary resolution, while restricting access via optional allowedArgs regex.

Quick Start: Virtual Sandbox

Use the virtual sandbox when an agent needs the standard bash/read/write tool surface without Docker:

import { InMemoryFs } from 'just-bash';
import { createBashTool, createVirtualSandbox } from '@deepagents/context';

const sandbox = await createBashTool({
  sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});

const result = await sandbox.sandbox.executeCommand('echo "hello"');
console.log(result.stdout); // hello

The virtual backend uses just-bash, so command execution and file IO stay in-process while still exercising the same createBashTool path used by agents. Pass javascript: true when the virtual sandbox should expose js-exec for JavaScript execution without bridging a host node binary.

When you want command groups such as sql run, tool validate, or marker remind inside a virtual sandbox, pass customCommands to createVirtualSandbox() and build them with Subcommand Builders.

Bash Tool Schema

createBashTool() from @deepagents/context returns a bash tool whose input schema requires two fields: command and reasoning. The LLM must provide a brief reason on every call; the owned tool enforces it at the Zod schema level.

interface BashToolInput {
  command: string;
  reasoning: string;
}

type WrappedBashTool = Tool<BashToolInput, CommandResult>;

The reasoning field is stripped before the command runs — it never influences execution. The AI SDK records it as part of the tool-call step input, so it remains visible in the response's content array for auditing. Calls missing reasoning fail schema validation before execute is invoked.

const sandbox = await createBashTool({
  sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});

// ✅ Accepted by the tool schema
await sandbox.tools.bash.execute(
  { command: 'ls /workspace', reasoning: 'List files to find the entry point' },
  {} as never,
);

// ❌ Rejected — AI SDK emits a tool-error with /reasoning/
await sandbox.tools.bash.execute(
  { command: 'ls /workspace' } as never,
  {} as never,
);

Type-safe consumers can import WrappedBashTool and BashToolInput directly from @deepagents/context.

Command Metadata

A bash hook can return host-only metadata alongside an optional replacement result. createBashTool() merges it with metadata already returned by sandbox decorators such as withStraceFileChanges().

const sandbox = await createBashTool({
  sandbox: backend,
  onAfterBashCall: ({ command }) =>
    command.startsWith('sql ')
      ? { meta: { formattedSql: 'SELECT 1' } }
      : undefined,
});

meta is preserved in the raw tool result for host-side consumers and stripped from model-visible output by the toolkit's shared toolset projection.

Custom Errors: BashException

Subclass BashException when you want a command or transform hook to fail with an explicit CommandResult shape.

import { BashException, type CommandResult } from '@deepagents/context';

class RateLimitError extends BashException {
  constructor(private readonly retryAfterMs: number) {
    super(`rate limited; retry in ${retryAfterMs}ms`);
  }

  format(): CommandResult {
    return {
      stdout: '',
      stderr: `${this.message}\n`,
      exitCode: 1,
    };
  }
}

createBashTool() catches BashException instances around sandbox execution and returns format() to the caller. Other error types still propagate normally.

Debug Logging

Gate bash logging behind your own environment variable when you construct the tool:

DEBUG_BASH=1 node scripts/run-demo.ts
# [bash] sql run main "SELECT 1"
# [bash] exit 1

The env var is gate-only — not a magic config:

const debug = Boolean(process.env.DEBUG_BASH);

await createBashTool({
  sandbox: bashInstance,
  onBeforeBashCall: ({ command }) => {
    if (debug) console.log(`[bash] ${command}`);
    return { command };
  },
  onAfterBashCall: ({ result }) => {
    if (debug && result.exitCode !== 0) {
      console.log(`[bash] exit ${result.exitCode}`);
    }
    return { result };
  },
});

Next Steps

On this page