mattpocock/sandcastlePublic

Orchestrate sandboxed coding agents in TypeScript with sandcastle.run()

AI summary: An interactive environment for testing, sharing, and demonstrating TypeScript code snippets.

Stars
7.3K
+33 today
Forks
734
Watchers
25
Open issues
84
Open PRs
37
Contributors
~12
Commits
1.2K
Branches
110

TypeScriptMITCreated Mar 17, 2026Last push 1mo agoLatest release v0.12.0+148 stars this week+175 this month

Star history

since Mar 22, 2026
02K4K6KMar 2026May 2026Jun 2026Aug 2026
7.3K stars as of Aug 7, 2026, tracked back to Mar 22, 2026. Historical curve reconstructed from public GitHub event archives, calibrated to the current total.

Contribution activity

commits per day, last 52 weeks
AugSepOctNovDecJanFebMarAprMayJunJulMonWedFri2025-08-03: 0 commits2025-08-04: 0 commits2025-08-05: 0 commits2025-08-06: 0 commits2025-08-07: 0 commits2025-08-08: 0 commits2025-08-09: 0 commits2025-08-10: 0 commits2025-08-11: 0 commits2025-08-12: 0 commits2025-08-13: 0 commits2025-08-14: 0 commits2025-08-15: 0 commits2025-08-16: 0 commits2025-08-17: 0 commits2025-08-18: 0 commits2025-08-19: 0 commits2025-08-20: 0 commits2025-08-21: 0 commits2025-08-22: 0 commits2025-08-23: 0 commits2025-08-24: 0 commits2025-08-25: 0 commits2025-08-26: 0 commits2025-08-27: 0 commits2025-08-28: 0 commits2025-08-29: 0 commits2025-08-30: 0 commits2025-08-31: 0 commits2025-09-01: 0 commits2025-09-02: 0 commits2025-09-03: 0 commits2025-09-04: 0 commits2025-09-05: 0 commits2025-09-06: 0 commits2025-09-07: 0 commits2025-09-08: 0 commits2025-09-09: 0 commits2025-09-10: 0 commits2025-09-11: 0 commits2025-09-12: 0 commits2025-09-13: 0 commits2025-09-14: 0 commits2025-09-15: 0 commits2025-09-16: 0 commits2025-09-17: 0 commits2025-09-18: 0 commits2025-09-19: 0 commits2025-09-20: 0 commits2025-09-21: 0 commits2025-09-22: 0 commits2025-09-23: 0 commits2025-09-24: 0 commits2025-09-25: 0 commits2025-09-26: 0 commits2025-09-27: 0 commits2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 0 commits2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-08: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 0 commits2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 0 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 0 commits2026-02-26: 0 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 0 commits2026-03-04: 0 commits2026-03-05: 0 commits2026-03-06: 0 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 0 commits2026-03-11: 0 commits2026-03-12: 0 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 16 commits2026-03-18: 21 commits2026-03-19: 0 commits2026-03-20: 3 commits2026-03-21: 6 commits2026-03-22: 0 commits2026-03-23: 0 commits2026-03-24: 37 commits2026-03-25: 115 commits2026-03-26: 84 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 0 commits2026-03-31: 0 commits2026-04-01: 27 commits2026-04-02: 34 commits2026-04-03: 3 commits2026-04-04: 1 commit2026-04-05: 8 commits2026-04-06: 0 commits2026-04-07: 0 commits2026-04-08: 1 commit2026-04-09: 1 commit2026-04-10: 40 commits2026-04-11: 6 commits2026-04-12: 0 commits2026-04-13: 37 commits2026-04-14: 44 commits2026-04-15: 47 commits2026-04-16: 13 commits2026-04-17: 12 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 0 commits2026-04-21: 33 commits2026-04-22: 37 commits2026-04-23: 11 commits2026-04-24: 22 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 2 commits2026-04-28: 1 commit2026-04-29: 12 commits2026-04-30: 0 commits2026-05-01: 2 commits2026-05-02: 2 commits2026-05-03: 1 commit2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 28 commits2026-05-07: 4 commits2026-05-08: 0 commits2026-05-09: 0 commits2026-05-10: 1 commit2026-05-11: 0 commits2026-05-12: 0 commits2026-05-13: 0 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 0 commits2026-05-19: 1 commit2026-05-20: 4 commits2026-05-21: 0 commits2026-05-22: 0 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 2 commits2026-05-26: 67 commits2026-05-27: 3 commits2026-05-28: 6 commits2026-05-29: 30 commits2026-05-30: 3 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 0 commits2026-06-05: 0 commits2026-06-06: 1 commit2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 0 commits2026-06-10: 0 commits2026-06-11: 4 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 5 commits2026-06-16: 1 commit2026-06-17: 0 commits2026-06-18: 21 commits2026-06-19: 0 commits2026-06-20: 1 commit2026-06-21: 0 commits2026-06-22: 0 commits2026-06-23: 0 commits2026-06-24: 0 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 3 commits2026-06-30: 0 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 0 commits2026-07-08: 0 commits2026-07-09: 0 commits2026-07-10: 0 commits2026-07-11: 0 commits2026-07-12: 0 commits2026-07-13: 0 commits2026-07-14: 0 commits2026-07-15: 0 commits2026-07-16: 0 commits2026-07-17: 0 commits2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 0 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 0 commits2026-07-31: 0 commits2026-08-01: 0 commits
864 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Permissive license

    MIT

What sandcastle does

Sandcastle provides a powerful, web-based sandbox specifically optimized for TypeScript development and exploration. It allows developers to write, compile, and execute TypeScript code entirely within the browser, offering real-time type checking and intellisense. The platform is designed for sharing code examples, reproducing bugs, and creating interactive tutorials without the need to set up a local development environment. Sandcastle integrates seamlessly with the TypeScript compiler API to ensure that snippets are evaluated exactly as they would be in a standard Node.js or browser environment.

Built for TypeScript developers of all skill levels, from beginners learning the language to experts testing complex type gymnastics. It is particularly useful for library maintainers and technical writers.

  • In-Browser Compilation: Compiles and executes TypeScript directly in the browser via WebAssembly.
  • Real-Time Type Checking: Provides instant feedback on type errors and syntax issues as you type.
  • Sharable Snippets: Generates unique URLs for code snippets, enabling easy sharing and collaboration.
  • Interactive Tutorials: Allows educators to build step-by-step interactive lessons embedding live code.
  • Configurable TSConfig: Supports custom compiler options to test snippets under specific strictness rules.

Where teams use it

Bug Reproduction

Creating minimal, reproducible examples of issues to share in bug reports or on StackOverflow.

Educational Content

Embedding live, editable TypeScript examples in documentation or tutorial websites.

Quick Prototyping

Testing out a new TypeScript feature or complex generic without spinning up a local project.

Library Demonstration

Providing an interactive playground for users to try out an open-source library.

Getting started: Visit the live site to start writing TypeScript immediately, or run `npm install sandcastle` to embed the sandbox in your own app.

README

main branch
Sandcastle

What Is Sandcastle?

A TypeScript library for orchestrating AI coding agents in isolated sandboxes:

  1. You invoke agents with a single sandcastle.run().
  2. Sandcastle handles sandboxing the agent with a configurable branch strategy.
  3. The commits made on the branches get merged back.

Sandcastle is provider-agnostic — it ships with built-in providers for Docker, Podman, and Vercel, and you can create your own. Great for parallelizing multiple AFK agents, creating review pipelines, or even just orchestrating your own agents.

Prerequisites

  • Git
  • A sandbox provider — Sandcastle needs an isolated environment to run agents in. Built-in options:
    • Docker Desktop — most common for local development
    • Podman — rootless alternative to Docker
    • Vercel — cloud-based Firecracker microVMs via @vercel/sandbox
    • Or create your own using createBindMountSandboxProvider or createIsolatedSandboxProvider

Quick start

  1. Install the package:
npm install --save-dev @ai-hero/sandcastle
  1. Run npx @ai-hero/sandcastle init. This scaffolds a .sandcastle directory with all the files needed.
npx @ai-hero/sandcastle init
  1. Edit .sandcastle/.env and fill in your default values for CLAUDE_CODE_OAUTH_TOKEN (run claude setup-token on your host to get one). To use an Anthropic API key instead, uncomment and fill in ANTHROPIC_API_KEY.
cp .sandcastle/.env.example .sandcastle/.env
  1. Run the .sandcastle/main.ts (or main.mts) file with npx tsx
npx tsx .sandcastle/main.ts
// 3. Run the agent via the JS API
import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

await run({
  agent: claudeCode("claude-opus-4-8"),
  sandbox: docker(), // or podman(), vercel(), or your own provider
  promptFile: ".sandcastle/prompt.md",
});

Sandbox Providers

Sandcastle uses a SandboxProvider to create isolated environments. The sandbox option on run(), interactive(), and createSandbox() accepts any provider, including noSandbox() — opt in to running the agent directly on the host when container isolation is undesired. Built-in providers:

Provider Import path Type Accepted by
Docker @ai-hero/sandcastle/sandboxes/docker Bind-mount run(), createSandbox(), interactive()
Podman @ai-hero/sandcastle/sandboxes/podman Bind-mount run(), createSandbox(), interactive()
Vercel @ai-hero/sandcastle/sandboxes/vercel Isolated run(), createSandbox(), interactive()
No-sandbox @ai-hero/sandcastle/sandboxes/no-sandbox None run(), createSandbox(), interactive()

Worktree methods (wt.run(), wt.interactive(), wt.createSandbox()) accept the same providers as their top-level counterparts. wt.interactive() defaults to noSandbox() when no sandbox is specified.

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 { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";

// Docker, Podman, and Vercel are interchangeable in run() and createSandbox():
await run({
  agent: claudeCode("claude-opus-4-8"),
  sandbox: docker(),
  prompt: "...",
});

// No-sandbox runs the agent directly on the host — accepted by run(),
// createSandbox(), and interactive(). Skips container isolation entirely:
await interactive({
  agent: claudeCode("claude-opus-4-8"),
  sandbox: noSandbox(),
  prompt: "...", // optional — omit to launch the TUI with no initial prompt
  cwd: "/path/to/other-repo", // optional — defaults to process.cwd()
});

You can also create your own provider using createBindMountSandboxProvider or createIsolatedSandboxProvider.

API

Sandcastle exports a programmatic run() function for use in scripts, CI pipelines, or custom tooling. The examples below use docker(), but any SandboxProvider works in its place.

import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

const result = await run({
  agent: claudeCode("claude-opus-4-8"),
  sandbox: docker(),
  promptFile: ".sandcastle/prompt.md",
});

console.log(result.iterations.length); // number of iterations executed
console.log(result.iterations); // per-iteration results with optional sessionId
console.log(result.commits); // array of { sha } for commits created
console.log(result.branch); // target branch name

All options

import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

const result = await run({
  // Agent provider — required. Pass a model string to claudeCode().
  // Optional second arg for provider-specific options like effort level.
  agent: claudeCode("claude-opus-4-8", { effort: "high" }),

  // Sandbox provider — required. Any SandboxProvider works (docker, podman, vercel, or custom).
  // Provider-specific config (like imageName, mounts) lives inside the provider factory call.
  sandbox: docker({
    imageName: "sandcastle:local",
    // Optional: override the UID/GID used for --user flag (defaults to host UID/GID).
    // Must match the UID baked into the image. Pre-flight check catches mismatches.
    // containerUid: 1000,
    // containerGid: 1000,
    // Optional: mount host directories into the sandbox (e.g. package manager caches)
    // hostPath supports absolute, tilde-expanded (~), and relative paths (resolved from cwd).
    // sandboxPath supports absolute and relative paths (resolved from the sandbox repo directory).
    mounts: [
      { hostPath: "~/.npm", sandboxPath: "/home/agent/.npm", readonly: true },
      { hostPath: "data", sandboxPath: "data" }, // mounts <cwd>/data → <sandbox-repo>/data
    ],
    // Optional: SELinux volume label — "z" (default, shared), "Z" (private), or false (none).
    // No-op on non-SELinux systems (Docker Desktop on macOS/Windows, Linux without SELinux).
    selinuxLabel: "z",
    // Optional: provider-level env vars merged at launch time
    env: { DOCKER_SPECIFIC: "value" },
    // Optional: attach container to Docker network(s) — string or string[]
    network: "my-network",
    // Optional: add the container user to supplementary groups via --group-add.
    // Accepts group names or numeric GIDs (e.g. for a bind-mounted Docker socket).
    groups: ["docker", 999],
    // Optional: expose host devices via --device. Each entry is a full device
    // spec in host[:container[:permissions]] form (e.g. "/dev/kvm").
    devices: ["/dev/kvm"],
    // Optional: limit CPU resources via --cpus. Fractional values allowed (e.g. 1.5).
    // cpus: 2,
  }),

  // Host repo directory — replaces process.cwd() as the anchor for
  // .sandcastle/ artifacts (worktrees, logs, env, patches) and git operations.
  // Relative paths resolve against process.cwd(). Defaults to process.cwd().
  cwd: "../other-repo",

  // Branch strategy — controls how the agent's changes relate to branches.
  // Defaults to { type: "head" } for bind-mount and { type: "merge-to-head" } for isolated providers.
  branchStrategy: { type: "branch", branch: "agent/fix-42" },

  // Prompt source — provide one of these, not both.
  // Note: promptFile resolves against process.cwd(), NOT cwd.
  promptFile: ".sandcastle/prompt.md", // path to a prompt file
  // prompt: "Fix issue #42 in this repo", // OR an inline prompt string

  // Values substituted for {{KEY}} placeholders in the prompt.
  promptArgs: {
    ISSUE_NUMBER: "42",
  },

  // Maximum number of agent iterations to run before stopping. Default: 1
  maxIterations: 5,

  // Display name for this run, shown as a prefix in log output.
  name: "fix-issue-42",

  // Lifecycle hooks grouped by where they run: host or sandbox.
  hooks: {
    host: {
      onWorktreeReady: [{ command: "cp .env.example .env" }],
      onSandboxReady: [{ command: "echo setup done" }],
    },
    sandbox: {
      onSandboxReady: [{ command: "npm install" }],
    },
  },

  // Host-relative file paths to copy into the sandbox before the container starts.
  // Not supported with branchStrategy: { type: "head" }.
  copyToWorktree: [".env"],

  // Override default timeouts for built-in lifecycle steps.
  // Unset keys keep their defaults.
  timeouts: {
    copyToWorktreeMs: 120_000, // default: 60_000
    gitSetupMs: 30_000, // default: 10_000
    commitCollectionMs: 60_000, // default: 30_000
    mergeToHostMs: 60_000, // default: 30_000
  },

  // How to record progress. Default: write to a file under .sandcastle/logs/
  logging: {
    type: "file",
    path: ".sandcastle/logs/my-run.log",
    // Optional: forward the agent's output stream to your own observability system.
    // Fires for each text chunk, tool call, and raw stdout line the agent
    // produces. Errors thrown by the callback are swallowed so a broken
    // forwarder cannot kill the run.
    onAgentStreamEvent: (event) => {
      // event is { type: "text" | "toolCall" | "raw", iteration, timestamp, ... }
      myLogger.info(event);
    },
    // Optional: append every raw stdout line the agent emits to the same
    // log file, interleaved with the human-readable output. Includes lines
    // the provider's stream parser would otherwise drop. Intended for
    // debugging stuck or unexpected agent behaviour.
    verbose: true,
  },
  // logging: { type: "stdout", verbose: true }, // OR terminal mode (verbose: raw lines to stdout)

  // String (or array of strings) the agent emits to end the iteration loop early.
  // Default: "<promise>COMPLETE</promise>"
  completionSignal: "<promise>COMPLETE</promise>",

  // Idle timeout in seconds — resets whenever the agent produces output. Default: 600 (10 minutes)
  idleTimeoutSeconds: 600,

  // Grace window in seconds after the agent emits a completion signal but
  // before its process has exited (a "hanging process" — typically a spawned
  // `gh`/git child or MCP server keeping stdout open). Resets on every
  // subsequent output line so trailing data is still captured. Default: 60
  completionTimeoutSeconds: 60,

  // Structured output — extract a typed payload from the agent's stdout.
  // Requires maxIterations === 1 and the tag must appear in the prompt.
  // output: Output.object({ tag: "result", schema: z.object({ answer: z.number() }) }),
  // output: Output.string({ tag: "summary" }),
});

console.log(result.iterations.length); // number of iterations executed
console.log(result.completionSignal); // matched signal string, or undefined if none fired
console.log(result.commits); // array of { sha } for commits created
console.log(result.branch); // target branch name

createSandbox() — reusable sandbox

Use createSandbox() when you need to run multiple agents (or multiple rounds of the same agent) inside a single sandbox. It creates the sandbox once, and you call sandbox.run() as many times as you need. This avoids repeated container startup costs and keeps all runs on the same branch.

Use run() instead when you only need a single one-shot invocation — it handles sandbox lifecycle automatically.

Basic single-run usage

import { createSandbox, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

await using sandbox = await createSandbox({
  branch: "agent/fix-42",
  sandbox: docker(),
});

const result = await sandbox.run({
  agent: claudeCode("claude-opus-4-8"),
  prompt: "Fix issue #42 in this repo.",
});

console.log(result.commits); // [{ sha: "abc123" }]

Multi-run implement-then-review

import { createSandbox, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

await using sandbox = await createSandbox({
  branch: "agent/fix-42",
  sandbox: docker(),
  hooks: { sandbox: { onSandboxReady: [{ command: "npm install" }] } },
});

// Step 1: implement
const implResult = await sandbox.run({
  agent: claudeCode("claude-opus-4-8"),
  promptFile: ".sandcastle/implement.md",
  maxIterations: 5,
});

// Step 2: review on the same branch, same container
const reviewResult = await sandbox.run({
  agent: claudeCode("claude-sonnet-4-6"),
  prompt: "Review the changes and fix any issues.",
});

Commits from all run() calls accumulate on the same branch. The sandbox container stays alive between runs, so installed dependencies and build artifacts persist.

sandbox.exec() lets the harness run shell commands directly in the same warm sandbox — handy for gating an implement step on a quick verification before kicking off the review:

await using sandbox = await createSandbox({
  branch: "agent/fix-42",
  sandbox: docker(),
  hooks: { sandbox: { onSandboxReady: [{ command: "npm install" }] } },
});

await sandbox.run({
  agent: claudeCode("claude-opus-4-8"),
  promptFile: ".sandcastle/implement.md",
  maxIterations: 5,
});

// Verify before review — non-zero exitCode is returned, not thrown.
const tests = await sandbox.exec("npm test");
if (tests.exitCode !== 0) {
  throw new Error(`Tests failed:\n${tests.stdout}\n${tests.stderr}`);
}

await sandbox.run({
  agent: claudeCode("claude-sonnet-4-6"),
  prompt: "Review the changes and fix any issues.",
});

cwd defaults to the sandbox repo path, matching interactive(). Pass cwd to override.

Automatic cleanup with await using

await using calls sandbox.close() automatically when the block exits. If the sandbox has uncommitted changes, the worktree is preserved on disk; if clean, both container and worktree are removed.

Manual close() with CloseResult

const sandbox = await createSandbox({
  branch: "agent/fix-42",
  sandbox: docker(),
});
// ... run agents ...
const closeResult = await sandbox.close();
if (closeResult.preservedWorktreePath) {
  console.log(`Worktree preserved at ${closeResult.preservedWorktreePath}`);
}

CreateSandboxOptions

Option Type Default Description
branch string Required. Explicit branch for the sandbox
sandbox SandboxProvider Required. Sandbox provider (e.g. docker(), podman())
cwd string process.cwd() Host repo directory — relative paths resolve against process.cwd()
hooks SandboxHooks Lifecycle hooks (host.*, sandbox.*) — run once at creation time
copyToWorktree string[] Host-relative file paths to copy into the sandbox at creation time
timeouts Timeouts Override built-in lifecycle step timeouts (copyToWorktreeMs, gitSetupMs, commitCollectionMs, mergeToHostMs)

Sandbox

Property / Method Type Description
branch string The branch the sandbox is on
worktreePath string Host path to the worktree
run(options) (SandboxRunOptions) => Promise<SandboxRunResult> Invoke an agent inside the existing sandbox
interactive(options) (SandboxInteractiveOptions) => Promise<SandboxInteractiveResult> Launch an interactive session in the sandbox
exec(cmd, options?) (command: string, options?: SandboxExecOptions) => Promise<ExecResult> Run a shell command in the sandbox. cwd defaults to the sandbox repo path. Non-zero exitCode is returned, not thrown.
close() () => Promise<CloseResult> Tear down the container and sandbox
[Symbol.asyncDispose] () => Promise<void> Auto teardown via await using

SandboxRunOptions

Option Type Default Description
agent AgentProvider Required. Agent provider (e.g. claudeCode("claude-opus-4-8"))
prompt string Inline prompt (mutually exclusive with promptFile)
promptFile string Path to prompt file (mutually exclusive with prompt)
promptArgs PromptArgs Key-value map for {{KEY}} placeholder substitution
maxIterations number 1 Maximum iterations to run
completionSignal string | string[] <promise>COMPLETE</promise> String(s) the agent emits to stop the iteration loop early
idleTimeoutSeconds number 600 Idle timeout in seconds — resets on each agent output event
completionTimeoutSeconds number 60 Grace window after the completion signal is seen but the agent process hasn't exited
name string Display name for the run
logging object file (auto-generated) { type: 'file', path } or { type: 'stdout' }
resumeSession string Resume a prior session by ID for agents that support resume. Incompatible with maxIterations > 1. Session file must exist on host.
signal AbortSignal Cancels the run when aborted; handle stays usable afterward

SandboxRunResult

Field Type Description
iterations IterationResult[] Per-iteration results (use .length for the count)
completionSignal string? The matched completion signal string, or undefined if none fired
stdout string Combined agent output from all iterations
commits { sha }[] Commits created during the run
logFilePath string? Path to the log file (only when logging to a file)
resume(prompt, options?) (prompt: string, options?: ResumeSandboxRunResultOptions) => Promise<SandboxRunResult> Continue the captured session for one iteration inside the same warm sandbox. Present only when the provider captured a session id.
fork(prompt, options?) (prompt: string, options?: ResumeSandboxRunResultOptions) => Promise<SandboxRunResult> Fork the captured session for one iteration inside the same warm sandbox. The parent session is left intact (ADR 0018).

CloseResult

Field Type Description
preservedWorktreePath string? Host path to the preserved worktree, set when it had uncommitted changes

createWorktree() — independent worktree lifecycle

Use createWorktree() when you need a worktree (git worktree) as an independent, first-class concept — separate from any sandbox. This is useful when you want to run an interactive session first and then hand the same worktree to a sandboxed AFK agent.

Only branch and merge-to-head strategies are accepted; head is a compile-time type error since it means no worktree.

Pass cwd to target a repo other than process.cwd(). Relative paths resolve against process.cwd(); absolute paths pass through. A CwdError is thrown if the path does not exist or is not a directory.

import { createWorktree } from "@ai-hero/sandcastle";

await using wt = await createWorktree({
  branchStrategy: { type: "branch", branch: "agent/fix-42" },
  copyToWorktree: ["node_modules"],
  cwd: "/path/to/other-repo", // optional — defaults to process.cwd()
});

console.log(wt.worktreePath); // host path to the worktree
console.log(wt.branch); // "agent/fix-42"

// Run an interactive session in the worktree (defaults to noSandbox)
await wt.interactive({
  agent: claudeCode("claude-opus-4-8"),
  prompt: "Explore the codebase and understand the bug.",
});

// Run an AFK agent in the worktree (sandbox is required)
const result = await wt.run({
  agent: claudeCode("claude-opus-4-8"),
  sandbox: docker({ imageName: "sandcastle:myrepo" }),
  prompt: "Fix issue #42.",
  maxIterations: 3,
});
console.log(result.commits); // commits made during the run

// Create a long-lived sandbox from the worktree
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

await using sandbox = await wt.createSandbox({
  sandbox: docker(),
  hooks: { sandbox: { onSandboxReady: [{ command: "npm install" }] } },
});

// sandbox.close() tears down the container only — the worktree stays
await sandbox.close();

// wt.close() cleans up the worktree

wt.close() checks for uncommitted changes: if the worktree is dirty, it's preserved on disk; if clean, it's removed. await using calls close() automatically. The worktree persists after run(), interactive(), and createSandbox() complete, so you can hand it to another agent or inspect it.

With branchStrategy: { type: "merge-to-head" }, each wt.run() / wt.interactive() merges the agent's commits back to the host's current branch before returning, and the worktree's source branch is preserved across calls so subsequent ones can reuse the same handle. (This differs from top-level run(), where the temp branch is deleted after the merge.)

Split ownership: When a sandbox is created via wt.createSandbox(), sandbox.close() tears down the container only — the worktree remains. wt.close() is responsible for worktree cleanup. This differs from the top-level createSandbox(), where sandbox.close() owns both container and worktree.

CreateWorktreeOptions

Option Type Default Description
branchStrategy WorktreeBranchStrategy Required. { type: "branch", branch } or { type: "merge-to-head" }
copyToWorktree string[] Host-relative file paths to copy into the worktree at creation time
timeouts Timeouts Override built-in lifecycle step timeouts (copyToWorktreeMs, gitSetupMs, commitCollectionMs, mergeToHostMs)

Worktree

Property / Method Type Description
branch string The branch the worktree is on
worktreePath string Host path to the worktree
run(options) (options: WorktreeRunOptions) => Promise<WorktreeRunResult> Run an AFK agent in the worktree (sandbox required)
interactive(options) (options: WorktreeInteractiveOptions) => Promise<InteractiveResult> Run an interactive agent session in the worktree
createSandbox(options) (options: WorktreeCreateSandboxOptions) => Promise<Sandbox> Create a long-lived sandbox backed by this worktree
close() () => Promise<CloseResult> Clean up the worktree (preserves if dirty)
[Symbol.asyncDispose] () => Promise<void> Auto cleanup via await using

WorktreeInteractiveOptions

Option Type Default Description
agent AgentProvider Required. Agent provider
sandbox AnySandboxProvider noSandbox() Sandbox provider (defaults to no sandbox)
prompt string Inline prompt (mutually exclusive with promptFile)
promptFile string Path to prompt file
name string Optional session name
hooks SandboxHooks Lifecycle hooks (host.*, sandbox.*)
promptArgs PromptArgs Key-value map for {{KEY}} placeholder substitution
env Record<string, string> Environment variables to inject into the sandbox
signal AbortSignal Cancel the session when aborted. The worktree is preserved on disk. Rejects with signal.reason.

WorktreeRunOptions

Option Type Default Description
agent AgentProvider Required. Agent provider
sandbox SandboxProvider Required. Sandbox provider (AFK agents must be sandboxed)
prompt string Inline prompt (mutually exclusive with promptFile)
promptFile string Path to prompt file
maxIterations number 1 Maximum iterations to run
completionSignal string | string[] Substring(s) to stop the iteration loop early
idleTimeoutSeconds number 600 Idle timeout in seconds
completionTimeoutSeconds number 60 Grace window after completion signal is seen but agent process hasn't exited
name string Optional run name
logging LoggingOption file Logging mode
hooks SandboxHooks Lifecycle hooks (host.*, sandbox.*)
promptArgs PromptArgs Key-value map for {{KEY}} placeholder substitution
env Record<string, string> Environment variables to inject into the sandbox
resumeSession string Resume a prior session by ID for agents that support resume. Incompatible with maxIterations > 1. Session file must exist on host.
signal AbortSignal Cancel the run when aborted. Kills the in-flight agent subprocess; the worktree is preserved on disk. Rejects with signal.reason.

WorktreeRunResult

Property Type Description
iterations IterationResult[] Per-iteration results (use .length for the count)
completionSignal string The matched completion signal, or undefined
stdout string Combined stdout output from all agent iterations
commits { sha: string }[] List of commits made by the agent during the run
branch string The branch name the agent worked on
logFilePath string Path to the log file, if logging was drained to a file

WorktreeCreateSandboxOptions

Option Type Default Description
sandbox SandboxProvider Required. Sandbox provider (e.g. docker())
hooks SandboxHooks Lifecycle hooks (host.*, sandbox.*)
copyToWorktree string[] Host-relative file paths to copy into the worktree at creation time
timeouts Timeouts Override built-in lifecycle step timeouts (copyToWorktreeMs, gitSetupMs, commitCollectionMs, mergeToHostMs)

How it works

Sandcastle uses a branch strategy configured on the sandbox provider to control how the agent's changes relate to branches. There are three strategies:

  • Head ({ type: "head" }) — The agent writes directly to the host working directory. No worktree, no branch indirection. This is the default for bind-mount providers like docker().
  • Merge-to-head ({ type: "merge-to-head" }) — Sandcastle creates a temporary branch in a git worktree. The agent works on the temp branch, and changes are merged back to HEAD when done. The temp branch is cleaned up after merge.
  • Branch ({ type: "branch", branch: "foo" }) — Commits land on an explicitly named branch in a git worktree. Re-running with the same branch reuses the existing worktree and fast-forwards it from origin when safe — see ADR 0003.

For bind-mount providers (like Docker), the worktree directory is bind-mounted into the container — the agent writes directly to the host filesystem through the mount, so no sync is needed.

From your point of view, you just configure branchStrategy: { type: 'branch', branch: 'foo' } on run(), and get a commit on branch foo once it's complete. All 100% local.

Prompts

Sandcastle uses a flexible prompt system. You write the prompt, and the engine executes it — no opinions about workflow, task management, or context sources are imposed.

Prompt resolution

You must provide exactly one of:

  1. prompt: "inline string" — pass an inline prompt directly via RunOptions
  2. promptFile: "./path/to/prompt.md" — point to a specific file via RunOptions

prompt and promptFile are mutually exclusive — providing both is an error. If neither is provided, run() throws an error asking you to supply one.

Inline prompts (prompt: "...") are passed to the agent literally. No {{KEY}} substitution, no !`command` expansion, no built-in {{SOURCE_BRANCH}} / {{TARGET_BRANCH}} injection. If you need values interpolated into an inline prompt, build the string in JavaScript (`Work on ${branch}…`). Passing promptArgs alongside an inline prompt is an error — switch to promptFile to use substitution.

The substitution and expansion features below apply only to prompts sourced from promptFile.

Convention: sandcastle init scaffolds .sandcastle/prompt.md and all templates explicitly reference it via promptFile: ".sandcastle/prompt.md". This is a convention, not an automatic fallback — Sandcastle does not read .sandcastle/prompt.md unless you pass it as promptFile.

Dynamic context with !`command`

Use !`command` expressions in your prompt to pull in dynamic context. Each expression is replaced with the command's stdout before the prompt is sent to the agent. All expressions in a prompt run in parallel for faster expansion.

Commands run inside the sandbox after sandbox.onSandboxReady hooks complete, so they see the same repo state the agent sees (including installed dependencies).

# Open issues

!`gh issue list --state open --label Sandcastle --json number,title,body,comments,labels --limit 100`

# Recent commits

!`git log --oneline -10`

If any command exits with a non-zero code, the run fails immediately with an error.

Prompt arguments with {{KEY}}

Use {{KEY}} placeholders in your prompt to inject values from the promptArgs option. This is useful for reusing the same prompt file across multiple runs with different parameters.

import { run } from "@ai-hero/sandcastle";

await run({
  promptFile: "./my-prompt.md",
  promptArgs: { ISSUE_NUMBER: 42, PRIORITY: "high" },
});

In the prompt file:

Work on issue #{{ISSUE_NUMBER}} (priority: {{PRIORITY}}).

Prompt argument substitution runs on the host before shell expression expansion, so {{KEY}} placeholders inside !`command` expressions are replaced first:

!`gh issue view {{ISSUE_NUMBER}} --json body -q .body`

A {{KEY}} placeholder with no matching prompt argument is an error. Unused prompt arguments produce a warning.

!`command` expansion only runs on shell blocks written in the prompt file itself. Any !`…` pattern that appears inside an argument value is treated as inert text — it won't be executed against the host shell. This makes it safe to pass user-authored content (issue titles, PR descriptions, docs excerpts) through promptArgs.

Built-in prompt arguments

Sandcastle automatically injects two built-in prompt arguments into every prompt:

Placeholder Value
{{SOURCE_BRANCH}} The branch the agent works on (determined by the branch strategy)
{{TARGET_BRANCH}} The host's active branch at run() time

Use them in your prompt without passing them via promptArgs:

You are working on {{SOURCE_BRANCH}}. When diffing, compare against {{TARGET_BRANCH}}.

Passing SOURCE_BRANCH or TARGET_BRANCH in promptArgs is an error — built-in prompt arguments cannot be overridden.

Early termination with <promise>COMPLETE</promise>

When the agent outputs <promise>COMPLETE</promise>, the orchestrator stops the iteration loop early. This is a convention you document in your prompt for the agent to follow — the engine never injects it.

This is useful for task-based workflows where the agent should stop once it has finished, rather than running all remaining iterations.

You can override the default signal by passing completionSignal to run(). It accepts a single string or an array of strings:

await run({
  // ...
  completionSignal: "DONE",
});

// Or pass multiple signals — the loop stops on the first match:
await run({
  // ...
  completionSignal: ["TASK_COMPLETE", "TASK_ABORTED"],
})(README truncated)

View on GitHub

Recent activity

commits and pull requests

Releases and announcements

43 total
  1. v0.12.0v0.12.0Jun 29, 2026

    ### Minor Changes - 1e15922: Bump default Claude Code model from `claude-opus-4-7` to `claude-opus-4-8`. The new default applies to the `DEFAULT_MODEL` constant, the `claude-code` agent entry surfaced by `sandcastle init`, and the scaffolded templates (`blank`, `parallel-planner`, `parallel-planner-with-review`). Passing an explicit model to `claudeCode(...)` is unaffected. - 0f577a4: Add `sandbox.exec(command, options?)` to the `Sandbox` handle returned by `createSandbox()` (and by `worktree.createSandbox()`). The method delegates to the provider handle's `exec()` and returns the full `ExecResult` — non-zero `exitCode` is surfaced, not thrown — so harnesses can run shell commands (tests, lints, custom verification gates) directly in the same warm sandbox between `run()` calls without reaching for the underlying provider handle. `cwd` defaults to the sandbox repo path so behavior is consistent across providers; pass `cwd` to override. ### Patch Changes - c505d49: Fix file-mode logging so streamed agent text flows as contiguous prose instead of one chunk per line. Added a dedicated `textChunk` streaming method to the display service (raw, no implied newline in file mode) and poin

  2. v0.10.0v0.10.0Jun 18, 2026

    ### Minor Changes - e445b70: Add `verbose` option to the `logging` configuration on `run()`, `createSandbox().run()`, and `createWorktree().run()`. When set to `true`: - In file mode (`{ type: "file", path, verbose: true }`), every raw stdout line the agent emits is appended verbatim to the same log file at `path` in real time, interleaved with the human-readable log output. - In stdout/terminal mode (`{ type: "stdout", verbose: true }`), raw lines are written to `process.stdout`. Includes lines the provider's stream parser would otherwise drop (e.g. tool-use blocks for unrecognised tools) — exactly what's needed to debug a stuck or unexpectedly silent agent. A new `{ type: "raw"; line; iteration; timestamp }` variant is also surfaced through `onAgentStreamEvent`, so callers forwarding to external observability systems get every raw line too.

  3. v0.9.0v0.9.0Jun 16, 2026

    ### Minor Changes - 47184de: Capture Claude Code subagent / workflow session transcripts to the host alongside the main session. Previously only the main `<sessionId>.jsonl` was copied off the sandbox; transcripts written by the `Agent` tool and the `Workflow` tool under `<sessionId>/subagents/agent-*.jsonl` were lost on teardown. They are now captured with the same sandbox→host `cwd` rewrite. Failure to capture an individual subagent transcript is best-effort and logs a warning; the main session capture remains fatal on failure. ### Patch Changes - 86aec83: `sandcastle init` now scaffolds `CLAUDE_CODE_OAUTH_TOKEN=` (with a commented `ANTHROPIC_API_KEY=` fallback) for the Claude Code agent, and the next-steps copy points users at `claude setup-token` instead of the closed issue #191. - 03dcc25: Guard `substitutePromptArgs` against `undefined`/`null` values in `promptArgs`. Previously, a present-but-nullish value (e.g. `{ TITLE: undefined }` from an orchestrator's `JSON.parse` output) bypassed the existence check and crashed with an unguarded `TypeError` on `.toString()`. Now surfaces a clean `PromptError` naming the offending key. `findMissingPromptArgKeys` also treats present-b

  4. v0.8.0v0.8.0Jun 11, 2026

    ### Minor Changes - cf92a17: Add `permissionMode` to `claudeCode()` and `approvalsReviewer` to `codex()` — provider-level options for AI-mediated per-tool approval, an alternative to full bypass for AFK host runs (`noSandbox()` + `run()`). `claudeCode({ permissionMode: "auto" })` emits `--permission-mode auto` instead of `--dangerously-skip-permissions`. Accepts any of Claude's permission modes: `default`, `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions`. `codex({ approvalsReviewer: "auto_review" })` swaps `--dangerously-bypass-approvals-and-sandbox` for `-a on-request -s danger-full-access -c approvals_reviewer="auto_review"` so Codex's reviewer agent evaluates each approval prompt. ### Patch Changes - 932302b: Bump the Codex default model from `gpt-5.4-mini` to `gpt-5.4` in `sandcastle init` scaffolding and the interactive agent picker. The previous default was underpowered for implementation work. - c6c3026: Fix `opencode()` interactive sessions (and the `init` scaffold's opencode `setupCommand`) seeding the prompt with `-p`, which is the `opencode run`/`attach` basic-auth password flag, not a prompt seed. Use `--prompt` (the TUI's long-form-only seed flag)

  5. v0.7.0v0.7.0May 30, 2026

    ### Minor Changes - 22113ca: `sandcastle init` now supports fully non-interactive setup. Every interactive prompt has a paired CLI flag (`--issue-tracker`, `--create-label`, `--build-image`, `--install-template-deps`) on top of the existing `--agent` / `--template` / `--sandbox` / `--model` / `--image-name`. When stdin is not a TTY and a flag is missing for a prompt that would otherwise fire, init fails fast with a message naming the missing flag instead of crashing on the prompt library. ### Patch Changes - 0b397a1: Strengthen the `simple-loop` and `sequential-reviewer` prompts so an empty pre-expanded `LIST_TASKS_COMMAND` result is treated as ground truth, not as a stale snapshot. The "do not re-query" hint now frames the filtered list as the sole source of truth, and the `# Done` completion criterion explicitly equates an empty list with completion. Prevents the agent from running its own unfiltered `gh issue list` when the filtered list is `[]`. - c6880a4: Fix `createSandbox` (and `createWorktree`) reusing a stale worktree when called twice for the same named branch. A reused worktree holds a local copy of the branch that never moves on its own, so a re-run loop (revie

Commits per week

last 52 weeks
2360Week of 2025-08-03: 0 commitsWeek of 2025-08-10: 0 commitsWeek of 2025-08-17: 0 commitsWeek of 2025-08-24: 0 commitsWeek of 2025-08-31: 0 commitsWeek of 2025-09-07: 0 commitsWeek of 2025-09-14: 0 commitsWeek of 2025-09-21: 0 commitsWeek of 2025-09-28: 0 commitsWeek of 2025-10-05: 0 commitsWeek of 2025-10-12: 0 commitsWeek of 2025-10-19: 0 commitsWeek of 2025-10-26: 0 commitsWeek of 2025-11-02: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 0 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 0 commitsWeek of 2026-02-22: 0 commitsWeek of 2026-03-01: 0 commitsWeek of 2026-03-08: 0 commitsWeek of 2026-03-15: 46 commitsWeek of 2026-03-22: 236 commitsWeek of 2026-03-29: 65 commitsWeek of 2026-04-05: 56 commitsWeek of 2026-04-12: 153 commitsWeek of 2026-04-19: 103 commitsWeek of 2026-04-26: 19 commitsWeek of 2026-05-03: 33 commitsWeek of 2026-05-10: 1 commitsWeek of 2026-05-17: 5 commitsWeek of 2026-05-24: 111 commitsWeek of 2026-05-31: 1 commitsWeek of 2026-06-07: 4 commitsWeek of 2026-06-14: 28 commitsWeek of 2026-06-21: 0 commitsWeek of 2026-06-28: 3 commitsWeek of 2026-07-05: 0 commitsWeek of 2026-07-12: 0 commitsWeek of 2026-07-19: 0 commitsWeek of 2026-07-26: 0 commitsAug 3, 2025Jul 26, 2026
864 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 0 commitsSun 1:00 — 0 commitsSun 2:00 — 0 commitsSun 3:00 — 0 commitsSun 4:00 — 0 commitsSun 5:00 — 0 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 1 commitsSun 10:00 — 0 commitsSun 11:00 — 0 commitsSun 12:00 — 3 commitsSun 13:00 — 5 commitsSun 14:00 — 1 commitsSun 15:00 — 0 commitsSun 16:00 — 0 commitsSun 17:00 — 0 commitsSun 18:00 — 0 commitsSun 19:00 — 0 commitsSun 20:00 — 0 commitsSun 21:00 — 0 commitsSun 22:00 — 0 commitsSun 23:00 — 0 commitsMon 0:00 — 0 commitsMon 1:00 — 0 commitsMon 2:00 — 0 commitsMon 3:00 — 0 commitsMon 4:00 — 0 commitsMon 5:00 — 0 commitsMon 6:00 — 0 commitsMon 7:00 — 1 commitsMon 8:00 — 1 commitsMon 9:00 — 2 commitsMon 10:00 — 0 commitsMon 11:00 — 1 commitsMon 12:00 — 0 commitsMon 13:00 — 0 commitsMon 14:00 — 0 commitsMon 15:00 — 0 commitsMon 16:00 — 0 commitsMon 17:00 — 16 commitsMon 18:00 — 7 commitsMon 19:00 — 9 commitsMon 20:00 — 9 commitsMon 21:00 — 3 commitsMon 22:00 — 0 commitsMon 23:00 — 0 commitsTue 0:00 — 0 commitsTue 1:00 — 0 commitsTue 2:00 — 0 commitsTue 3:00 — 0 commitsTue 4:00 — 0 commitsTue 5:00 — 0 commitsTue 6:00 — 6 commitsTue 7:00 — 10 commitsTue 8:00 — 7 commitsTue 9:00 — 8 commitsTue 10:00 — 18 commitsTue 11:00 — 17 commitsTue 12:00 — 5 commitsTue 13:00 — 14 commitsTue 14:00 — 41 commitsTue 15:00 — 22 commitsTue 16:00 — 7 commitsTue 17:00 — 11 commitsTue 18:00 — 13 commitsTue 19:00 — 12 commitsTue 20:00 — 9 commitsTue 21:00 — 0 commitsTue 22:00 — 0 commitsTue 23:00 — 0 commitsWed 0:00 — 0 commitsWed 1:00 — 0 commitsWed 2:00 — 0 commitsWed 3:00 — 0 commitsWed 4:00 — 0 commitsWed 5:00 — 12 commitsWed 6:00 — 2 commitsWed 7:00 — 1 commitsWed 8:00 — 16 commitsWed 9:00 — 30 commitsWed 10:00 — 23 commitsWed 11:00 — 33 commitsWed 12:00 — 13 commitsWed 13:00 — 42 commitsWed 14:00 — 31 commitsWed 15:00 — 20 commitsWed 16:00 — 11 commitsWed 17:00 — 10 commitsWed 18:00 — 10 commitsWed 19:00 — 27 commitsWed 20:00 — 18 commitsWed 21:00 — 4 commitsWed 22:00 — 0 commitsWed 23:00 — 2 commitsThu 0:00 — 0 commitsThu 1:00 — 0 commitsThu 2:00 — 0 commitsThu 3:00 — 0 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 15 commitsThu 7:00 — 16 commitsThu 8:00 — 8 commitsThu 9:00 — 39 commitsThu 10:00 — 43 commitsThu 11:00 — 22 commitsThu 12:00 — 2 commitsThu 13:00 — 2 commitsThu 14:00 — 14 commitsThu 15:00 — 0 commitsThu 16:00 — 6 commitsThu 17:00 — 3 commitsThu 18:00 — 0 commitsThu 19:00 — 3 commitsThu 20:00 — 8 commitsThu 21:00 — 3 commitsThu 22:00 — 0 commitsThu 23:00 — 0 commitsFri 0:00 — 0 commitsFri 1:00 — 0 commitsFri 2:00 — 0 commitsFri 3:00 — 0 commitsFri 4:00 — 0 commitsFri 5:00 — 0 commitsFri 6:00 — 0 commitsFri 7:00 — 0 commitsFri 8:00 — 0 commitsFri 9:00 — 1 commitsFri 10:00 — 10 commitsFri 11:00 — 16 commitsFri 12:00 — 13 commitsFri 13:00 — 7 commitsFri 14:00 — 9 commitsFri 15:00 — 26 commitsFri 16:00 — 7 commitsFri 17:00 — 1 commitsFri 18:00 — 4 commitsFri 19:00 — 2 commitsFri 20:00 — 16 commitsFri 21:00 — 0 commitsFri 22:00 — 0 commitsFri 23:00 — 0 commitsSat 0:00 — 0 commitsSat 1:00 — 0 commitsSat 2:00 — 0 commitsSat 3:00 — 0 commitsSat 4:00 — 0 commitsSat 5:00 — 0 commitsSat 6:00 — 0 commitsSat 7:00 — 0 commitsSat 8:00 — 4 commitsSat 9:00 — 8 commitsSat 10:00 — 0 commitsSat 11:00 — 0 commitsSat 12:00 — 0 commitsSat 13:00 — 1 commitsSat 14:00 — 0 commitsSat 15:00 — 1 commitsSat 16:00 — 0 commitsSat 17:00 — 0 commitsSat 18:00 — 0 commitsSat 19:00 — 1 commitsSat 20:00 — 0 commitsSat 21:00 — 3 commitsSat 22:00 — 2 commitsSat 23:00 — 0 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.

Who is committing

last 52 weeks
Maintainer commits844 (71%)
Community commits349 (29%)

1,193 commits in total over the last year.

DateListRankStars gained
May 1, 2026daily#24+60
  • freeCodeCamp/freeCodeCamp

    freeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.

    453.6K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    385.5K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • anomalyco/opencode

    The open source coding agent.

    194.7K stars · TypeScript