Cloudflare Container sandboxes for coding agents and databases.
npm install @dotdo/sandbox
# or
pnpm add @dotdo/sandbox- Coding Agent Sandboxes: Run Claude Code, OpenCode, and Codex in isolated containers
- Database Sandboxes: PostgreSQL, SQLite, and DuckDB containers
- RPC Client: WebSocket-based client for browser and Node.js
- Durable Objects: State management via Cloudflare Durable Objects
import { getSandbox } from '@cloudflare/sandbox'
import { createClaudeCodeSandbox } from '@dotdo/sandbox/agents/claude-code'
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Create sandbox from container binding
const sandbox = getSandbox(env.CLAUDE_CODE_SANDBOX, 'my-session')
const claude = createClaudeCodeSandbox({
sandbox,
apiKey: env.ANTHROPIC_API_KEY,
permissionMode: 'acceptEdits',
})
await claude.init()
const result = await claude.run('Create a REST API with Express')
await claude.close()
return Response.json({ output: result.result })
},
}import { createPostgresContainer } from '@dotdo/sandbox/databases/postgres'
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const pg = createPostgresContainer(env.POSTGRES_CONTAINER)
await pg.connect()
const users = await pg.query<{ id: number; name: string }>(
'SELECT * FROM users WHERE active = $1',
[true]
)
await pg.close()
return Response.json({ users })
},
}import { createSandboxClient } from '@dotdo/sandbox/client'
const client = createSandboxClient({
auth: 'your-api-key',
})
// Create and use a sandbox
const instance = await client.agents.create({ type: 'claude-code' })
const result = await client.agents.run(instance.id, 'Hello world')
console.log(result.stdout)See the examples/ directory for complete Worker examples:
- basic-worker - Simple Worker using Claude Code sandbox
- database-worker - Worker using PostgreSQL, SQLite, and DuckDB containers
Run Claude Code in a container for autonomous coding tasks.
import { getSandbox } from '@cloudflare/sandbox'
import { ClaudeCodeSandbox, createClaudeCodeSandbox } from '@dotdo/sandbox/agents/claude-code'
// Using factory function
const claude = createClaudeCodeSandbox({
sandbox: getSandbox(env.CLAUDE_CODE_SANDBOX, sessionId),
apiKey: env.ANTHROPIC_API_KEY,
permissionMode: 'acceptEdits',
model: 'claude-sonnet-4-20250514',
workingDirectory: '/workspace',
})
// Or using class directly
const claude = new ClaudeCodeSandbox({
sandbox,
apiKey: env.ANTHROPIC_API_KEY,
permissionMode: 'bypassPermissions',
maxThinkingTokens: 10000,
})
// Initialize (starts container, verifies claude CLI)
await claude.init()
// Run a prompt
const result = await claude.run('Build a todo app with React', {
timeout: 600000, // 10 minutes
})
// Access results
console.log(result.success) // boolean
console.log(result.result) // parsed JSON output
console.log(result.stdout) // raw stdout
console.log(result.sessionId) // for conversation continuity
// Execute shell commands directly
const execResult = await claude.exec('npm install')
// Clone and work on a repository
const repoResult = await claude.runOnRepo(
'https://github.com/user/repo',
'Add unit tests for the auth module',
{ branch: 'main', shallow: true }
)
// Multi-turn conversation
const results = await claude.runConversation([
'Create a new Express server',
'Add authentication middleware',
'Write tests for the auth routes',
])
// File operations
await claude.writeFile('/workspace/config.json', JSON.stringify({ port: 3000 }))
const content = await claude.readFile('/workspace/package.json')
const files = await claude.listFiles('/workspace')
// Clean up
await claude.close()Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
sandbox |
Sandbox |
required | Sandbox instance from @cloudflare/sandbox |
apiKey |
string |
- | Anthropic API key |
permissionMode |
'default' | 'acceptEdits' | 'bypassPermissions' |
'acceptEdits' |
Permission handling mode |
model |
string |
'claude-sonnet-4-20250514' |
Model to use |
maxThinkingTokens |
number |
0 |
Max tokens for extended thinking |
workingDirectory |
string |
'/workspace' |
Working directory in container |
commandTimeout |
number |
300000 |
Command timeout in ms |
systemPrompt |
string |
- | Custom system prompt |
allowedTools |
string[] |
- | Tools to allow |
disallowedTools |
string[] |
- | Tools to disallow |
Run OpenCode with multiple provider support.
import { createOpenCodeSandbox } from '@dotdo/sandbox/agents/opencode'
const opencode = createOpenCodeSandbox({
sandbox: getSandbox(env.OPENCODE_SANDBOX, sessionId),
provider: 'anthropic', // or 'openai', 'ollama'
apiKey: env.ANTHROPIC_API_KEY,
})
await opencode.init()
const result = await opencode.run('Refactor this codebase to use TypeScript')
await opencode.close()Run OpenAI Codex CLI for code generation.
import { createCodexSandbox } from '@dotdo/sandbox/agents/codex'
const codex = createCodexSandbox({
sandbox: getSandbox(env.CODEX_SANDBOX, sessionId),
apiKey: env.OPENAI_API_KEY,
approvalMode: 'full-auto',
})
await codex.init()
const result = await codex.run('Generate a Python script to process CSV files')
await codex.close()Create any agent sandbox by type:
import { createAgentSandbox, isAgentType } from '@dotdo/sandbox/agents'
const type = 'claude-code' // or 'opencode', 'codex'
if (isAgentType(type)) {
const agent = createAgentSandbox(type, {
sandbox: getSandbox(env.SANDBOX, sessionId),
apiKey: env.API_KEY,
})
}Full-featured PostgreSQL in a container.
import { PostgresContainer, createPostgresContainer } from '@dotdo/sandbox/databases/postgres'
const pg = createPostgresContainer(env.POSTGRES_CONTAINER, {
database: 'myapp',
username: 'postgres',
password: 'secret',
})
// Connect (waits for container startup)
await pg.connect()
// Check startup time
console.log(`Cold start: ${pg.getStartupTime()}ms`)
// Execute DDL
await pg.execute(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT
)
`)
// Insert with parameters (SQL injection safe)
await pg.execute(
'INSERT INTO users (email, name) VALUES ($1, $2)',
['alice@example.com', 'Alice']
)
// Query with typed results
interface User {
id: number
email: string
name: string
}
const users = await pg.query<User>('SELECT * FROM users WHERE id = $1', [1])
// Query with full metadata
const result = await pg.queryWithMetadata<User>('SELECT * FROM users')
console.log(result.rows) // User[]
console.log(result.rowCount) // number
console.log(result.fields) // column metadata
console.log(result.command) // 'SELECT'
// Transactions
await pg.transaction(async (execute) => {
await execute('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1])
await execute('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2])
// Auto-commits on success, auto-rollback on error
})
// Database administration
const version = await pg.version()
const tables = await pg.listTables()
const columns = await pg.describeTable('users')
const databases = await pg.listDatabases()
await pg.createDatabase('newdb')
// Health check
const isHealthy = await pg.ping()
// Clean up
await pg.close()Lightweight SQLite in a container.
import { createSQLiteContainer } from '@dotdo/sandbox/databases/sqlite'
const sqlite = createSQLiteContainer(env.SQLITE_CONTAINER, {
database: '/data/app.db',
})
await sqlite.connect()
// SQLite operations
await sqlite.execute('CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)')
await sqlite.execute('INSERT INTO tasks (title) VALUES (?)', ['Learn containers'])
const tasks = await sqlite.query<{ id: number; title: string }>(
'SELECT * FROM tasks'
)
// SQLite-specific features
const tableInfo = await sqlite.tableInfo('tasks')
const tables = await sqlite.listTables()
await sqlite.close()DuckDB for analytics and OLAP workloads.
import { createDuckDBContainer } from '@dotdo/sandbox/databases/duckdb'
const duckdb = createDuckDBContainer(env.DUCKDB_CONTAINER)
await duckdb.connect()
// Create and query tables
await duckdb.execute(`
CREATE TABLE events (
event_type VARCHAR,
user_id VARCHAR,
timestamp TIMESTAMP,
properties JSON
)
`)
// Analytics queries
const stats = await duckdb.query(`
SELECT event_type, COUNT(*) as count
FROM events
GROUP BY event_type
ORDER BY count DESC
`)
// Load from Parquet files (DuckDB specialty)
const parquetData = await duckdb.query(`
SELECT * FROM read_parquet('https://example.com/data.parquet')
`)
// Export data
const csvExport = await duckdb.exportData('events', 'csv')
await duckdb.close()WebSocket-based client for remote sandbox management.
import {
createSandboxClient,
connectSandbox,
SANDBOX_DO_URL,
} from '@dotdo/sandbox/client'
// Create client for sandbox.do
const client = createSandboxClient({
url: 'wss://sandbox.do/rpc', // or custom endpoint
auth: 'your-api-key',
timeout: 30000,
reconnect: true,
})
// Agent operations
const agent = await client.agents.create({
type: 'claude-code',
size: 'standard-2',
env: { DEBUG: 'true' },
})
const status = await client.agents.get(agent.id)
const agents = await client.agents.list()
const result = await client.agents.run(agent.id, 'Create a hello world app', {
model: 'claude-sonnet-4-20250514',
permissionMode: 'acceptEdits',
timeout: 300000,
})
const execResult = await client.agents.exec(agent.id, 'npm install', {
cwd: '/workspace',
timeout: 60000,
})
await client.agents.writeFile(agent.id, '/workspace/config.json', '{}')
const content = await client.agents.readFile(agent.id, '/workspace/package.json')
await client.agents.cloneRepo(agent.id, 'https://github.com/user/repo', {
branch: 'main',
shallow: true,
})
await client.agents.stop(agent.id)
// Database operations
const db = await client.databases.create({
type: 'postgres',
size: 'standard-1',
})
const rows = await client.databases.query<{ id: number }>(
db.id,
'SELECT * FROM users WHERE active = $1',
[true]
)
const { rowsAffected } = await client.databases.execute(
db.id,
'UPDATE users SET active = $1 WHERE id = $2',
[false, 123]
)
await client.databases.stop(db.id)
// Session management
const session = await client.sessions.current()
const quota = await client.sessions.quota()
// Connect directly to a specific sandbox
const sandbox = connectSandbox('sandbox-id', { auth: 'your-api-key' })
await sandbox.exec('ls -la')
const status = await sandbox.status()
await sandbox.stop()The sandbox.do Worker also exposes a REST API:
# Health check
GET /health
# List agents
GET /api/agents
Authorization: Bearer your-api-key
# Create agent
POST /api/agents
Content-Type: application/json
Authorization: Bearer your-api-key
{"type": "claude-code", "size": "standard-2"}
# Get agent
GET /api/agents/{id}
Authorization: Bearer your-api-key
# Run prompt
POST /api/agents/{id}/run
Content-Type: application/json
Authorization: Bearer your-api-key
{"prompt": "Create a hello world app", "options": {"timeout": 300000}}
# Execute command
POST /api/agents/{id}/exec
Content-Type: application/json
Authorization: Bearer your-api-key
{"command": "npm install", "options": {"cwd": "/workspace"}}
# Stop agent
DELETE /api/agents/{id}
Authorization: Bearer your-api-key
# Database operations
GET /api/databases
POST /api/databases
GET /api/databases/{id}
POST /api/databases/{id}/query
POST /api/databases/{id}/execute
DELETE /api/databases/{id}
# Session info
GET /api/sessions/current
GET /api/sessions/quotaExample wrangler.toml for a sandbox worker:
name = "my-sandbox-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
# Claude Code container
[[containers]]
binding = "CLAUDE_CODE_SANDBOX"
class_name = "ClaudeCodeContainer"
image = "./dockerfiles/Dockerfile.claude-code"
max_instances = 5
default_port = 8080
sleep_after = "10m"
# PostgreSQL container
[[containers]]
binding = "POSTGRES_CONTAINER"
class_name = "PostgresContainer"
image = "./dockerfiles/Dockerfile.postgres"
max_instances = 10
default_port = 8080
sleep_after = "10m"
# Durable Object for state
[[durable_objects.bindings]]
name = "SANDBOX_DO"
class_name = "SandboxDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SandboxDO"]
[vars]
# Set API keys via wrangler secret or .dev.varsPre-built wrangler configs are available:
import {
CLAUDE_CODE_WRANGLER_CONFIG,
OPENCODE_WRANGLER_CONFIG,
CODEX_WRANGLER_CONFIG,
POSTGRES_WRANGLER_CONFIG,
SQLITE_WRANGLER_CONFIG,
DUCKDB_WRANGLER_CONFIG,
} from '@dotdo/sandbox'
console.log(POSTGRES_WRANGLER_CONFIG)
// {
// binding: 'POSTGRES_CONTAINER',
// className: 'PostgresContainer',
// image: './dockerfiles/Dockerfile.postgres',
// defaultPort: 8080,
// maxInstances: 10,
// sleepAfter: '10m',
// size: 'standard-1',
// }- Node.js >= 18.0.0
- pnpm
- Cloudflare account with Containers enabled
# Install dependencies
pnpm install
# Build
pnpm build
# Run unit tests
pnpm test
# Run E2E tests (requires Cloudflare credentials)
pnpm test:e2e
# Run integration tests (requires SANDBOX_API_KEY)
pnpm test:integration
# Run all tests
pnpm test:all| Variable | Required | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
For E2E tests | Cloudflare API token with Workers permissions |
CLOUDFLARE_ACCOUNT_ID |
For E2E tests | Your Cloudflare account ID |
SANDBOX_API_KEY |
For integration tests | API key for sandbox.do |
- Unit tests (
pnpm test): Fast, isolated tests with mocks - E2E tests (
pnpm test:e2e): Deploy to Cloudflare preview and test live infrastructure - Integration tests (
pnpm test:integration): Test against live sandbox.do deployment - Workers tests (
pnpm test:workers): Cloudflare Workers-specific tests using vitest-pool-workers
This project uses GitHub Actions for continuous integration:
- Unit Tests: Run on every push and PR
- E2E Tests: Deploy to Cloudflare preview and run end-to-end tests
- Integration Tests: Test against live sandbox.do (requires secrets)
- Node.js Matrix: Test across Node.js 18, 20, and 22
Configure these secrets in your GitHub repository:
CLOUDFLARE_API_TOKEN: API token with Workers edit permissionsCLOUDFLARE_ACCOUNT_ID: Your Cloudflare account IDSANDBOX_API_KEY: (Optional) API key for integration tests
MIT