# How to Create a Custom Sandbox Provider in Sandcastle Using `createBindMountSandboxProvider`

> Learn to create a custom sandbox provider in Sandcastle with createBindMountSandboxProvider. Implement key methods and pass your provider to run() or createSandbox() for flexible sandboxing.

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

---

**Use `createBindMountSandboxProvider` from [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts) to define a factory that returns a `BindMountSandboxHandle` implementing `exec`, `copyFileIn`, `copyFileOut`, and `close` methods, then pass the provider to `run()` or `createSandbox()`.**

Sandcastle is a flexible code execution framework that abstracts containerized runtimes through the `SandboxProvider` interface. If you need to integrate a custom execution environment—whether a `chroot` jail, LXC container, or VM—`createBindMountSandboxProvider` offers the standard factory to register your implementation with the framework. This guide walks through the contract requirements and implementation details found in the `mattpocock/sandcastle` repository.

## Understanding the Sandbox Abstraction

Sandcastle distinguishes between sandbox types, with **bind-mount** being the simplest: the host file system mounts directly into the sandbox runtime, allowing agents to read and write files without overlay filesystems. The core factory for these providers resides in [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts) (lines 303-311), exporting `createBindMountSandboxProvider` which accepts a configuration object defining how to spawn and manage your runtime.

## Configuring the Provider Factory

The factory expects an object with four key properties: `name`, optional `env`, optional `sandboxHomedir`, and the mandatory `create` function.

- **`name`**: A human-readable identifier like `"docker"` or `"chroot-alpine"`.
- **`env`**: Extra environment variables merged with user-supplied values.
- **`sandboxHomedir`**: Path to `$HOME` inside the sandbox, required for containers with fixed home directories.
- **`create`**: An async function receiving `BindMountCreateOptions` (defined at lines 66-78 in [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts)) and returning a `BindMountSandboxHandle`.

The `BindMountCreateOptions` interface supplies:
- `worktreePath`: Host path to the project directory.
- `repoPath`: Original repository root.
- `mounts`: Pre-calculated bind-mounts prepared by Sandcastle.
- `env`: Process environment variables.

## Implementing the `BindMountSandboxHandle` Contract

Your `create` function must return an object satisfying the `BindMountSandboxHandle` interface (lines 23-64 in [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts)). The handle acts as the runtime controller with these required methods:

- **`exec(command, opts?)`**: Execute a command inside the sandbox, returning stdout, stderr, and exit code. Output should stream line-by-line for real-time processing.
- **`copyFileIn(hostPath, sandboxPath)`**: Copy files from the host into the sandbox.
- **`copyFileOut(sandboxPath, hostPath)`**: Copy files from the sandbox back to the host.
- **`close()`**: Asynchronously clean up the runtime (stop containers, remove temp directories, etc.).

Optionally implement **`interactiveExec(args, opts)`** to support TTY sessions, forwarding stdin/stdout/stderr between the user and sandbox process.

## Custom Provider Implementation Example

Below is a minimal `chroot`-based provider that satisfies the contract. It creates an isolated directory, bind-mounts the worktree, and implements all required handle methods:

```typescript
// src/sandboxes/chrootProvider.ts
import {
  createBindMountSandboxProvider,
  type BindMountCreateOptions,
  type BindMountSandboxHandle,
} from "../SandboxProvider.js";

export const chrootProvider = createBindMountSandboxProvider({
  name: "chroot-alpine",
  
  create: async (opts: BindMountCreateOptions): Promise<BindMountSandboxHandle> => {
    const chrootBase = "/tmp/sandcastle-chroot";
    
    // Setup: Ensure chroot directory exists
    const fs = await import("fs/promises");
    await fs.mkdir(chrootBase, { recursive: true });
    
    // Calculate sandbox-side worktree path
    const sandboxWorktree = `${chrootBase}${opts.worktreePath}`;
    
    return {
      worktreePath: sandboxWorktree,
      
      exec: async (command) => {
        const { execFile } = await import("node:child_process");
        return new Promise((resolve, reject) => {
          execFile(
            "chroot",
            [chrootBase, "sh", "-c", command],
            { env: { ...opts.env, HOME: "/root" } },
            (error, stdout, stderr) => {
              if (error) reject(error);
              else resolve({
                stdout: stdout?.toString() ?? "",
                stderr: stderr?.toString() ?? "",
                exitCode: 0,
              });
            }
          );
        });
      },
      
      interactiveExec: async (args, { stdin, stdout, stderr }) => {
        const { spawn } = await import("node:child_process");
        const proc = spawn("chroot", [chrootBase, ...args], {
          stdio: [stdin, stdout, stderr],
        });
        return new Promise((resolve) => {
          proc.on("close", (code) => resolve({ exitCode: code ?? 0 }));
        });
      },
      
      copyFileIn: async (hostPath, sandboxPath) => {
        const { execFile } = await import("node:child_process");
        await new Promise<void>((res, rej) => {
          execFile("cp", [hostPath, `${chrootBase}${sandboxPath}`], (err) => 
            err ? rej(err) : res()
          );
        });
      },
      
      copyFileOut: async (sandboxPath, hostPath) => {
        const { execFile } = await import("node:child_process");
        await new Promise<void>((res, rej) => {
          execFile("cp", [`${chrootBase}${sandboxPath}`, hostPath], (err) => 
            err ? rej(err) : res()
          );
        });
      },
      
      close: async () => {
        const { rm } = await import("fs/promises");
        await rm(chrootBase, { recursive: true, force: true });
      },
    };
  },
});

```

## Integrating Your Provider

Once exported, use your provider exactly like built-in implementations by invoking it and passing the handle to Sandcastle's execution API:

```typescript
import { run } from "sandcastle";
import { chrootProvider } from "./sandboxes/chrootProvider.js";

await run({
  agent: myAgent,
  sandbox: chrootProvider(),
});

```

The provider also works with `interactive()` for TTY sessions and `createSandbox()` for direct handle access.

## Reference Implementations

Study the official implementations for complex runtime patterns:

- **Docker**: [`src/sandboxes/docker.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/sandboxes/docker.ts) (lines 85-138) demonstrates container lifecycle management, volume mapping, and the Docker CLI integration.
- **Podman**: [`src/sandboxes/podman.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/sandboxes/podman.ts) mirrors the Docker pattern targeting the `podman` binary.

Both follow the identical `BindMountSandboxHandle` contract, proving the abstraction works across container engines.

## Summary

- **`createBindMountSandboxProvider`** in [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts) is the factory for bind-mount sandbox integrations.
- The `create` function receives `BindMountCreateOptions` and must return a `BindMountSandboxHandle`.
- Required handle methods include `exec`, `copyFileIn`, `copyFileOut`, and `close`.
- Reference implementations in [`src/sandboxes/docker.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/sandboxes/docker.ts) and [`src/sandboxes/podman.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/sandboxes/podman.ts) demonstrate production patterns.
- Custom providers integrate seamlessly with `run()`, `interactive()`, and `createSandbox()`.

## Frequently Asked Questions

### What is the difference between `createBindMountSandboxProvider` and other sandbox types?

Bind-mount providers map the host worktree directly into the sandbox runtime, allowing native file access. Isolated sandboxes (not covered by this factory) use overlay filesystems or ephemeral storage, while `"none"` disables sandboxing entirely. According to [`src/startSandbox.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/startSandbox.ts), the dispatcher selects bind-mount when the provider follows this specific contract.

### Do I need to implement `interactiveExec` for my custom provider?

No. `interactiveExec` is optional per the `BindMountSandboxHandle` interface (lines 23-64 in [`src/SandboxProvider.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxProvider.ts)). However, implementing it enables TTY support for `interactive()` sessions. If omitted, Sandcastle falls back to non-interactive execution.

### How do I handle environment variables in my custom sandbox?

Merge `opts.env` (provided in `BindMountCreateOptions`) with any runtime-specific defaults in your `exec` implementation. The factory's optional `env` property adds static variables to all executions, while `opts.env` contains dynamic values from the Sandcastle runtime.

### Can I use `createBindMountSandboxProvider` for cloud-based sandboxes?

Yes, provided the remote environment supports bind-mount semantics or you simulate them via synchronization. The contract only requires file copy methods and command execution; the underlying runtime can be local Docker, remote Kubernetes, or even a VM managed via SSH, as implemented in the handle's methods.