All skills
av avatar

/sandcastle

@b676141
by Ivan Charapanauav/skills16 stars
2

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.

Use this Skill: https://skilld.dev/gh/av/skills/sandcastle

This session only. Nothing lands on disk.

SKILL.md

β‰ˆ134 tokens always: the name and description. β‰ˆ3.5k when used: this file. β‰ˆ3.1k more on demand in 3 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/Dockerfile

Quick 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 on

Core 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() and createSandbox() 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 typed

The 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 exits

RunResult

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 image

sandcastle 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

  1. Sandbox providers are subpath imports β€” import { docker } from "@ai-hero/sandcastle/sandboxes/docker", never from the main entry point.

  2. noSandbox() only works with interactive() β€” run() and createSandbox() reject it. AFK work requires real isolation.

  3. head strategy is incompatible with isolated providers β€” throws at runtime. Use merge-to-head or branch instead.

  4. copyToWorktree is incompatible with head strategy β€” head mode bind-mounts the host directory directly, no worktree exists.

  5. resumeSession requires maxIterations: 1 β€” resume applies to iteration 1 only.

  6. Structured output requires maxIterations: 1 β€” and the XML tag must appear in the prompt text. Sandcastle validates both at startup.

  7. Inline prompts skip all processing β€” no {{KEY}} substitution, no !`command` expansion. Passing promptArgs with an inline prompt is an error.

  8. Built-in prompt args (SOURCE_BRANCH, TARGET_BRANCH) cannot be overridden β€” attempting to do so is a runtime error.

  9. Docker UID must match image UID β€” sandcastle runs a pre-flight check and errors on mismatch. Default is host UID or 1000.

  10. 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:

  1. Pipeline script β€” TypeScript file using sandcastle's API (e.g., pipeline.ts or .sandcastle/run.ts)
  2. Prompt files β€” .sandcastle/*.md prompt templates with {{ARG}} placeholders and !`command` expressions as needed
  3. Docker setup β€” .sandcastle/Dockerfile if not already present (or instruct user to run npx sandcastle init)
  4. package.json script β€” e.g., "pipeline": "npx tsx .sandcastle/run.ts"

Verification

After generating a pipeline:

  1. Confirm @ai-hero/sandcastle is in package.json dependencies
  2. Confirm sandbox provider imports use subpath syntax
  3. Confirm prompt files exist at the referenced paths
  4. Confirm any {{ARG}} placeholders in prompts have matching promptArgs
  5. If using Docker: confirm .sandcastle/Dockerfile exists or sandcastle init was run
  6. If using structured output: confirm maxIterations: 1 and the XML tag appears in the prompt

Source: SKILL.md on GitHub

1 warning1d3 checks Β· Risk SAFE
  • Gen Agent Trust Hub1d

    The skill provides documentation and configuration for the Sandcastle SDK, which orchestrates AI coding agents in isolated environments. It includes features for lifecycle hooks, prompt-based shell expressions, and sandbox management.

  • Socket1d

    1 alert: gptSecurity

  • Snyk1d

    Risk: LOW Β· No issues

Signed by skilld at b676141. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Activeupdated 5 months ago

README badge

README badge for av/skills/sandcastle