# How to Create a Reusable Sandbox with `createSandbox()` for Multiple Agent Runs in Sandcastle

> Learn how to create a reusable sandbox with createSandbox() in Sandcastle for efficient multiple agent runs. Persist state and lifecycle for streamlined workflows.

- Repository: [Matt Pocock/sandcastle](https://github.com/mattpocock/sandcastle)
- Tags: how-to-guide
- Published: 2026-05-24

---

**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](https://github.com/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`](https://github.com/mattpocock/sandcastle/blob/main/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 a `SandboxRunResult`.
- **`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`](https://github.com/mattpocock/sandcastle/blob/main/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`](https://github.com/mattpocock/sandcastle/blob/main/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`](https://github.com/mattpocock/sandcastle/blob/main/src/createSandbox.ts)) constructs the `Sandbox` object. It captures:
- **`branch`**, **`worktreePath`**, **`hostRepoDir`**, **`sandboxRepoDir`**
- **`sandboxLayer`**: The Effect-layer providing sandbox-scoped services
- **`providerHandle`**: 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:
1. Calls `providerHandle.close()` to stop the container.
2. Checks for uncommitted changes via `WorktreeManager.hasUncommittedChanges`.
3. Preserves the work-tree if dirty (printing the path on `SIGINT`/`SIGTERM`), otherwise removes it via `WorktreeManager.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:

```typescript
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):

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

```typescript
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 single `Sandbox` handle 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 `sandbox` option.
- **`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`](https://github.com/mattpocock/sandcastle/blob/main/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.