sandcastle
Skill28 starsChanged 8 months agoMITView on GitHub
Orchestrate AI coding agents (Claude Code, Codex, OpenCode) in isolated sandboxes using the @ai-hero/sandcastle SDK. Use when the user needs to (1) run agents AFK in Docker/Podman containers, (2) build multi-agent pipelines with plan-execute-review patterns, (3) run parallel agents on separate worktrees, (4) create iterative agent loops with maxIterations, (5) extract structured output from agent runs, (6) set up sandcastle in a new or existing project, or (7) write prompt files with template args and shell expressions.
- Reads credentials
- Installs packages
2 other projects use a version of this.
Adopted by
Most recently taken up by
- henkisdabro/sandcastle-kittheir own version, first seen Oct 20, 2026
- will-ness-ai/skillstheir own version, first seen Aug 13, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- will-ness-ai/skills★ 170same name
- henkisdabro/sandcastle-kit★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository25 other files it gives its agents.
- agent-integration-testingSkill
- annealSkill
- article-factorySkill
- autoresearchSkill
- boost-modulesSkill
- bugbashSkill
- bughuntSkill
- catalog-deslopSkill
- disciplineSkill
- factual-communicationSkill
- ideateSkill
- improve-skillSkill
- make-videoSkill
- mosaic-writingSkill
- preact-buildless-frontendSkill
- pull-llamacpp-modelSkill
- run-llmsSkill
- superclaudeSkill
- timeboxed-iteratingSkill
- timeframe-researchSkill
- tinygradSkill
- turso-dbSkill
- use-coding-agentsSkill
- workgraphSkill
- workmachineSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 441
When asked to build a sandcastle pipeline:
- Distinctive: line 450
After generating a pipeline:
- Very specific: line 456
If using Docker: confirm .sandcastle/Dockerfile exists or sandcastle init was run
- Very specific: line 457
If using structured output: confirm maxIterations: 1 and the XML tag appears in the prompt
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Sandcastle
Sandcastle (@ai-hero/sandcastle) orchestrates AI coding agents inside isolated sandbox environments. It manages git worktrees, boots containers, runs agents with structured prompts, collects commits, and merges results back to branches — all from a single run() call.
Installation
npm i @ai-hero/sandcastle
npx sandcastle init # scaffolds .sandcastle/ with Dockerfile and prompt templates
npx sandcastle docker build-image # builds the Docker image from .sandcastle/DockerfileQuick Start
import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
const result = await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});
console.log(result.commits); // [{ sha: "abc123" }, ...]
console.log(result.branch); // branch the agent worked onCore Concepts
The Four Entry Points
| Function | Use Case |
run(options) | One-shot agent invocation. Full lifecycle managed automatically. |
createSandbox({ branch, sandbox }) | Reusable sandbox on an explicit branch. Call .run() multiple times. |
createWorktree({ branchStrategy }) | Independent git worktree. Call .run(), .interactive(), or .createSandbox(). |
interactive(options) | Interactive terminal session with an agent. Human-in-the-loop. |
Agent Providers
import { claudeCode, codex, opencode, pi } from "@ai-hero/sandcastle";
claudeCode() // default: claude-opus-4-7
claudeCode("claude-sonnet-4-6") // specify model
claudeCode("claude-sonnet-4-6", { effort: "low" }) // with options
codex("o4-mini")Sandbox Providers
Sandbox providers are subpath imports — not from the main entry point:
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
import { vercel } from "@ai-hero/sandcastle/sandboxes/vercel";
import { daytona } from "@ai-hero/sandcastle/sandboxes/daytona";
import { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";Two categories:
- Bind-mount (docker, podman): mount the worktree into a container. Default branch strategy:
head. - Isolated (vercel, daytona): sync files into a remote environment. Default branch strategy:
merge-to-head. - noSandbox: host-only, no isolation. Only valid with
interactive()—run()andcreateSandbox()reject it.
Branch Strategies
| Strategy | Behavior | Default For |
{ type: "head" } | Agent writes directly to host working directory. Bind-mount only. | docker, podman |
{ type: "merge-to-head" } | Temp branch, auto-merged back to HEAD on completion. | vercel, daytona |
{ type: "branch", branch: "feat/x" } | Commits land on a named branch. Optional baseBranch. | — |
Pipeline Patterns
Pattern 1: Simple One-Shot
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});Pattern 2: Iteration Loop
Agent runs multiple iterations, checking for a completion signal between each:
await run({
name: "worker",
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
maxIterations: 5,
branchStrategy: { type: "merge-to-head" },
copyToWorktree: ["node_modules"],
hooks: {
sandbox: {
onSandboxReady: [{ command: "npm install" }],
},
},
});The agent sees a completion signal instruction in the prompt. When it outputs <promise>COMPLETE</promise> (default), iteration stops. Otherwise it runs up to maxIterations.
Pattern 3: Sequential Reviewer
Reuse one sandbox for implement → review → fix cycles:
const sandbox = await createSandbox({
branch: "feature/my-branch",
sandbox: docker(),
});
await sandbox.run({
agent: claudeCode("claude-sonnet-4-6"),
promptFile: "./.sandcastle/implement.md",
});
await sandbox.run({
agent: claudeCode("claude-opus-4-7"),
promptFile: "./.sandcastle/review.md",
});
await sandbox.close();Pattern 4: Parallel Agents
Multiple agents work in parallel on separate worktrees, then a merge agent combines results:
const tasks = issues.map(async (issue) => {
const wt = await createWorktree({
branchStrategy: { type: "branch", branch: `fix/${issue.id}` },
});
const result = await wt.run({
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
prompt: `Fix issue: ${issue.title}\n\n${issue.description}`,
});
await wt.close();
return result;
});
const results = await Promise.allSettled(tasks);Pattern 5: Structured Output
Extract typed data from agent runs:
import { Output } from "@ai-hero/sandcastle";
import { z } from "zod";
const result = await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/plan.md",
maxIterations: 1, // required for structured output
output: Output.object({
tag: "plan",
schema: z.object({
issues: z.array(z.object({
title: z.string(),
priority: z.enum(["high", "medium", "low"]),
})),
}),
}),
});
console.log(result.output.issues); // fully typedThe prompt must contain the XML tag (e.g., <plan>) — sandcastle validates this at startup. The agent wraps its structured response in that tag. Also available: Output.string({ tag }) for plain string extraction.
Pattern 6: Interactive Then Automated
Explore interactively, then automate implementation:
const wt = await createWorktree({
branchStrategy: { type: "branch", branch: "feature/new" },
});
await wt.interactive({
agent: claudeCode("claude-opus-4-7"),
sandbox: noSandbox(), // noSandbox only valid with interactive()
});
await wt.run({
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
promptFile: "./.sandcastle/implement.md",
});
await wt.close();Prompt System
Inline Prompts
await run({
agent: claudeCode(),
sandbox: docker(),
prompt: "Refactor the auth module to use dependency injection",
});Inline prompts skip all processing — no {{KEY}} substitution, no shell expressions. Passing promptArgs with an inline prompt is an error.
Prompt Files
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
promptArgs: { ISSUE_TITLE: "Fix login bug", ISSUE_BODY: "Users can't log in" },
});Prompt files support two features:
1. Template arguments — {{KEY}} placeholders replaced from promptArgs:
Fix the following issue:
Title: {{ISSUE_TITLE}}
Body: {{ISSUE_BODY}}2. Shell expressions — !`command` evaluated inside the sandbox before each iteration:
The current test output is:
!`npm test 2>&1 | tail -50`
The current git diff is:
!`git diff`Shell expressions run per-iteration, so each iteration sees fresh state.
Built-in args (auto-injected, cannot be overridden): {{SOURCE_BRANCH}}, {{TARGET_BRANCH}}.
Processing order: Prompt file resolution → Arg substitution (host, once) → Shell expansion (sandbox, per iteration).
Configuration
Docker Options
docker({
imageName: "my-custom-image",
containerUid: 1000,
containerGid: 1000,
mounts: [
{ hostPath: "~/.npmrc", sandboxPath: "~/.npmrc", readonly: true },
{ hostPath: "./secrets", sandboxPath: "/app/secrets" },
],
env: { NODE_ENV: "development", CI: "true" },
network: "host",
selinuxLabel: "z",
})Hooks
Lifecycle hooks run commands at specific points:
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
hooks: {
host: {
onWorktreeReady: [{ command: "cp .env.local .sandcastle/.env" }],
onSandboxReady: [{ command: "echo 'sandbox is up'" }],
},
sandbox: {
onSandboxReady: [
{ command: "npm install", timeoutMs: 120_000 },
{ command: "npm run build", sudo: false },
],
},
},
});Execution order: copyToWorktree → host.onWorktreeReady (sequential) → sandbox created → host.onSandboxReady + sandbox.onSandboxReady (parallel).
Timeouts
| Parameter | Default |
idleTimeoutSeconds | 600 (10 min) |
| Hook commands | 60s (per-hook timeoutMs) |
| Git setup | 10s |
| Commit collection | 30s |
| Merge to host | 30s |
| Shell expression expansion | 30s |
timeouts.copyToWorktreeMs | 60s |
Completion Signal
await run({
completionSignal: "<done>FINISHED</done>", // custom signal
// or: completionSignal: ["<done>FINISHED</done>", "<done>SKIPPED</done>"],
// default: "<promise>COMPLETE</promise>"
});Cancellation
const controller = new AbortController();
setTimeout(() => controller.abort("timeout"), 300_000);
await run({
agent: claudeCode(),
sandbox: docker(),
prompt: "Fix all lint errors",
signal: controller.signal,
});Automatic Cleanup
Both Sandbox and Worktree support Symbol.asyncDispose:
await using sandbox = await createSandbox({
branch: "feature/x",
sandbox: docker(),
});
// automatically cleaned up when scope exitsRunResult
Every run() returns:
{
iterations: IterationResult[]; // per-iteration details
completionSignal?: string; // matched signal, or undefined if maxIterations hit
stdout: string; // combined agent output
commits: { sha: string }[]; // commits made by agent
branch: string; // branch agent worked on
logFilePath?: string; // log file path (file logging mode)
preservedWorktreePath?: string; // set when worktree had uncommitted changes
output?: T; // present when Output config was provided
}Session Management
Claude Code sessions can be captured and resumed across runs:
import { hostSessionStore, transferSession } from "@ai-hero/sandcastle";
const result = await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});
// Resume a prior session in a new run
await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/continue.md",
resumeSession: "session-id-from-prior-run",
maxIterations: 1, // required when resuming
});CLI Commands
npx sandcastle init # scaffold .sandcastle/ directory
npx sandcastle docker build-image # build Docker image from .sandcastle/Dockerfile
npx sandcastle docker remove-image # remove the Docker image
npx sandcastle podman build-image # build with Podman
npx sandcastle podman remove-image # remove Podman imagesandcastle init offers templates:
- blank — minimal single
run()call - simple-loop — iteration loop picking work items
- sequential-reviewer — implement-then-review via
createSandbox() - parallel-planner — plan/execute/merge with parallel agents
- parallel-planner-with-review — parallel planner plus review phase
Critical Rules
- Sandbox providers are subpath imports —
import { docker } from "@ai-hero/sandcastle/sandboxes/docker", never from the main entry point. noSandbox()only works withinteractive()—run()andcreateSandbox()reject it. AFK work requires real isolation.headstrategy is incompatible with isolated providers — throws at runtime. Usemerge-to-headorbranchinstead.copyToWorktreeis incompatible withheadstrategy — head mode bind-mounts the host directory directly, no worktree exists.resumeSessionrequiresmaxIterations: 1— resume applies to iteration 1 only.- Structured output requires
maxIterations: 1— and the XML tag must appear in the prompt text. Sandcastle validates both at startup. - Inline prompts skip all processing — no
{{KEY}}substitution, no!`command`expansion. PassingpromptArgswith an inlinepromptis an error. - Built-in prompt args (
SOURCE_BRANCH,TARGET_BRANCH) cannot be overridden — attempting to do so is a runtime error. - Docker UID must match image UID — sandcastle runs a pre-flight check and errors on mismatch. Default is host UID or 1000.
- Worktrees are preserved on error — if the agent leaves uncommitted changes or an error occurs, the worktree is not cleaned up. The path is returned in
preservedWorktreePath.
See references/api-types.md for full type signatures and references/prompt-system.md for prompt file details.
Deliverables
When asked to build a sandcastle pipeline:
- Pipeline script — TypeScript file using sandcastle's API (e.g.,
pipeline.tsor.sandcastle/run.ts) - Prompt files —
.sandcastle/*.mdprompt templates with{{ARG}}placeholders and!`command`expressions as needed - Docker setup —
.sandcastle/Dockerfileif not already present (or instruct user to runnpx sandcastle init) - package.json script — e.g.,
"pipeline": "npx tsx .sandcastle/run.ts"
Verification
After generating a pipeline:
- Confirm
@ai-hero/sandcastleis inpackage.jsondependencies - Confirm sandbox provider imports use subpath syntax
- Confirm prompt files exist at the referenced paths
- Confirm any
{{ARG}}placeholders in prompts have matchingpromptArgs - If using Docker: confirm
.sandcastle/Dockerfileexists orsandcastle initwas run - If using structured output: confirm
maxIterations: 1and the XML tag appears in the prompt
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

