How to Create a Reusable Sandbox with `createSandbox()` for Multiple Agent Runs in Sandcastle
Use createSandbox() to instantiate a single Sandbox handle that persists across multiple run() calls, maintaining filesystem state, environment variables, and container lifecycle until explicitly closed.
The createSandbox() factory function in the mattpocock/sandcastle repository creates a long-lived execution environment that you can reuse for sequential agent runs. Instead of spinning up fresh containers for each task, you initialize the sandbox once and invoke run() as needed, allowing subsequent agents to access files written by previous runs.
Understanding the Sandbox Handle Interface
In src/createSandbox.ts, the factory returns an object implementing the Sandbox interface. This handle exposes three primary methods that control the sandbox lifecycle:
run(options): Executes an agent inside the existing sandbox container, returning aSandboxRunResult.interactive(options): Starts an interactive session (such as a REPL) within the same sandbox environment.close(): Tears down the sandbox container and removes the associated git work-tree, unless uncommitted changes are detected.
Because the handle maintains references to the underlying container and work-tree, you can invoke run() multiple times without reinitializing the infrastructure.
Architecture of a Reusable Sandbox
The reusability of a sandbox depends on several architectural decisions in the source code. When you call createSandbox(), the system performs the following steps to ensure state persists between runs.
Work-tree Creation and Mounting
First, createSandbox() resolves the repository root and creates a git work-tree on the specified branch using WorktreeManager.create (see lines ≈ 99‑101 in src/createSandbox.ts). This work-tree is bound-mounted into the sandbox container, meaning the agent can read and write files directly to the host filesystem. Because the work-tree persists for the lifetime of the handle, any files an agent creates remain available for subsequent runs.
Provider Selection and Container Startup
The sandbox option you provide selects a provider—docker, podman, vercel, isolated, or none—which determines the execution environment. The startSandbox() function in src/startSandbox.ts initializes the container (or isolated process) and returns a sandboxLayer along with a providerHandle. This layer encapsulates the provider-specific services and remains active for all subsequent operations.
Handle Construction and State Persistence
After the container starts, buildSandboxHandle() (lines ≈ 198‑260 in src/createSandbox.ts) constructs the Sandbox object. It captures:
branch,worktreePath,hostRepoDir,sandboxRepoDirsandboxLayer: The Effect-layer providing sandbox-scoped servicesproviderHandle: The low-level container handle used for teardown and interactive execution
When you call sandbox.run() multiple times, the system reuses the same sandboxLayer and applyToHost callback. Only the orchestrate logic changes between runs, ensuring no new containers launch and no state is lost.
Lifecycle Management and Teardown
The close() method executes the doClose function (lines ≈ 758‑891), which:
- Calls
providerHandle.close()to stop the container. - Checks for uncommitted changes via
WorktreeManager.hasUncommittedChanges. - Preserves the work-tree if dirty (printing the path on
SIGINT/SIGTERM), otherwise removes it viaWorktreeManager.remove.
Signal handlers registered at creation (lines ≈ 640‑666) ensure abrupt termination preserves your work-tree when necessary.
Implementing Multiple Agent Runs
The following patterns demonstrate how to leverage createSandbox() for sequential agent execution while maintaining state.
Basic Reuse with Docker
Create the sandbox once and run multiple agents against the same filesystem:
import { createSandbox } from "sandcastle";
import { docker } from "sandcastle/sandboxes/docker";
import { claudeCode } from "sandcastle/agents/claude";
// Initialize once
const sandbox = await createSandbox({
branch: "dev-feature",
sandbox: docker({ imageName: "sandcastle:latest" }),
copyToWorktree: ["scripts/", "config/.env"],
});
// First run
await sandbox.run({
agent: claudeCode("claude-opus-4-7"),
promptFile: "prompts/first.md",
maxIterations: 3,
});
// Second run reuses same container and work-tree
await sandbox.run({
agent: claudeCode("claude-sonnet-3.5"),
prompt: "Refactor the code generated in the previous run.",
maxIterations: 1,
});
// Cleanup when finished
await sandbox.close();
Isolated Provider with Host Sync
For environments without containers, use the isolated provider. Changes sync back to the host automatically via applyToHost (implemented around lines ≈ 540‑556):
import { createSandbox } from "sandcastle";
import { isolated } from "sandcastle/sandboxes/no-sandbox";
const sandbox = await createSandbox({
branch: "tmp-branch",
sandbox: isolated(),
});
await sandbox.run({
agent: claudeCode("claude-3.5-sonnet"),
prompt: "Generate a README for this repo.",
});
// Files automatically sync to host after each run
await sandbox.close();
Interactive Session Followed by Batch Runs
You can mix interactive and batch modes on the same sandbox handle:
import { createSandbox } from "sandcastle";
import { docker } from "sandcastle/sandboxes/docker";
import { claudeCode } from "sandcastle/agents/claude";
const sandbox = await createSandbox({
branch: "interactive-demo",
sandbox: docker({ imageName: "sandcastle:latest" }),
});
// Interactive session
await sandbox.interactive({
agent: claudeCode("claude-opus-4-7"),
prompt: "You are now in an interactive REPL. Respond with JSON only.",
name: "dev-session",
});
// Sandbox remains usable for batch runs after interactive exits
await sandbox.run({
agent: claudeCode("claude-sonnet-3.5"),
prompt: "Analyze the files created during the interactive session.",
});
await sandbox.close();
Summary
createSandbox()creates a singleSandboxhandle that lives for multiple agent executions.- The git work-tree and container remain active between
run()calls, preserving filesystem state. - Supported providers include Docker, Podman, Vercel, isolated, and none, configured via the
sandboxoption. close()terminates the container and conditionally removes the work-tree based on uncommitted changes.- Signal handlers ensure graceful preservation of dirty work-trees on abrupt shutdown.
Frequently Asked Questions
What happens to file changes between runs in a reusable sandbox?
Files written during one run() call persist in the git work-tree and are visible to subsequent runs. Because the work-tree is bind-mounted into the container (or directly accessible in isolated mode), agents can build upon previous outputs, read generated artifacts, or modify configuration files that affect future executions.
Can I switch agents between runs on the same sandbox?
Yes. The Sandbox handle is agent-agnostic. You can pass different agent configurations to each run() call—for example, using claude-opus-4-7 for an initial generation task and claude-sonnet-3.5 for a refactoring pass—while maintaining access to the same filesystem and environment state.
How do I preserve the work-tree after closing the sandbox?
The close() method automatically preserves the work-tree if uncommitted changes exist. According to the doClose implementation in src/createSandbox.ts (lines ≈ 758‑891), the system checks WorktreeManager.hasUncommittedChanges and skips removal if the tree is dirty. You can also rely on SIGINT/SIGTERM handlers (lines ≈ 640‑666) to preserve the work-tree path on abrupt termination.
What is the difference between run() and interactive()?
run() executes an agent for a defined number of iterations and returns when complete, making it ideal for batch processing. interactive() starts a persistent session (such as a REPL) that maintains state until manually exited, suitable for debugging or conversational workflows. Both methods operate on the same underlying container and work-tree, so you can alternate between them on a single sandbox handle.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →