Skip to main content
Sandcastle is a TypeScript library that runs coding agents, such as Claude Code and Codex, against your git repo in a sandbox. You pass run() an agent, a sandbox provider, and a prompt. Sandcastle copies the repo into the sandbox, runs the agent, and merges the agent’s commits back into your current branch. This page adds a Superserve sandbox provider to your project, so each run gets its own isolated VM. The agent’s API key stays in a Superserve secret and never enters the sandbox.

Prerequisites

  • Node.js 22.18 or later, so node runs the .ts files directly
  • A git repo with at least one commit
  • A Superserve API key. See API keys.
  • An Anthropic API key for Claude Code, or an OpenAI API key for Codex

Setup

Sandcastle is a TypeScript library. Install it and the Superserve SDK in your repo:
The scripts on this page use top-level await, so set "type": "module" in package.json. Then put your keys in .env:
.env
Keep .env and Sandcastle’s run files out of git. Sandcastle copies only committed files into the sandbox:
.gitignore

Store the API key as a secret

Store your Anthropic key as a team secret once. Superserve gives the sandbox a stand-in token and attaches the real key only on requests to Anthropic.
For the Python version, run pip install superserve and export the keys from .env before you run python create_secret.py.

Add the sandbox provider

Sandcastle’s createIsolatedSandboxProvider turns a create function into a sandbox provider. This provider creates a Superserve sandbox for each run, and sends Sandcastle’s commands and file copies to it. It tags each sandbox with the metadata sandcastle: "true", keeps a heartbeat tag up to date while the run uses the sandbox, and deletes the sandbox when the run ends. Save it as .sandcastle/superserve.ts:
.sandcastle/superserve.ts
superserve() takes the same options as Sandbox.create, such as fromTemplate, secrets, network, and metadata, plus: The provider also sets idleTimeoutSeconds to 10 minutes, unless you pass another value, so a sandbox with no command running stops billing. A paused sandbox can still belong to a run, for example while a host.onSandboxReady hook runs on your machine. The next command resumes it. If you pass timeoutSeconds, set it longer than any run. It pauses the sandbox even while a command runs, and Sandcastle then stops the run with an error.

Run Claude Code

The superserve/claude-code template has Claude Code and git installed. Bind the secret to ANTHROPIC_API_KEY, and pass the agent and the prompt to run():
.sandcastle/main.ts
Sandcastle writes the agent’s progress to the log file. Output from a run with claude-sonnet-4-6 (trimmed):
The commit is now on your current branch, main in this run. Sandcastle sets the sandbox’s git user.name and user.email from your repo, so the agent’s commits have your identity.

Run Codex

The superserve/node-22 template has Node.js and git. Install Codex in a sandbox.onSandboxReady hook, which runs in the sandbox before the agent starts. Codex reads its key from CODEX_API_KEY when it runs in exec mode. First, add your OpenAI key to .env as OPENAI_API_KEY, and store it as a secret with the OpenAI provider:
Then bind the secret to CODEX_API_KEY:
.sandcastle/codex.ts
Output from a run with gpt-5.5 (trimmed):
To skip the install in every run, build a template with Codex installed, and pass its name as fromTemplate.

Delete leftover sandboxes

The provider deletes its sandbox when a run ends, also when the run fails. If the Node.js process stops before that, for example on kill -9, the sandbox stays. The sandbox’s status does not tell you if a run still uses it. Use the heartbeat tag instead: while the run’s process is alive, the provider sets sandcastle-heartbeat to the current time every minute, also while the sandbox is paused. These scripts delete only tagged sandboxes with no heartbeat for 15 minutes, so they do not delete a sandbox that a run in another process or on a teammate’s machine still uses:
The heartbeat comes from the clock of the machine that runs Sandcastle, and the scripts compare it with the clock of the machine that runs them. Keep both clocks in sync. If the machine that runs Sandcastle sleeps for more than 15 minutes during a run, for example a closed laptop, the run looks stale, and the scripts delete its sandbox. Keep that machine awake during runs, or increase STALE_MS. You can also pass autoDeleteSeconds to superserve(). Superserve then deletes a sandbox after it stays paused for that long, and it does not check the heartbeat. Set it longer than any pause during a run, such as a long host.onSandboxReady hook. See Lifecycle for these options, and Metadata tags for more about filters.

Good to know

  • Sandcastle is TypeScript only. The Python SDK can create the secrets and delete leftover sandboxes, but it can’t run Sandcastle.
  • The provider supports run(). It does not support interactive(), because it does not implement interactiveExec.
  • Commands in the sandbox run as root. Claude Code refuses --dangerously-skip-permissions as root unless IS_SANDBOX=1 is set, so the provider sets it in every sandbox.
  • Sandcastle copies the repo into the sandbox with git bundle, so the template must have git. Uncommitted changes and ignored files are not copied.
  • The agent’s output reaches Sandcastle line by line while it runs. If the agent prints nothing for 10 minutes, Sandcastle stops the run. Change this with the idleTimeoutSeconds option of run(). It is a different option from the sandbox’s idleTimeoutSeconds in superserve().
  • The provider writes the prompt and copied directories to temporary files in the sandbox’s /tmp, and deletes them after use.
  • To limit what the agent can reach, pass network rules as network. Allow the agent’s API host, and any hosts that the onSandboxReady hooks need, such as the npm registry.

Resources

Sandcastle

Agents, branch strategies, hooks, and the run() options.

Secrets

How secrets keep API keys out of the sandbox.