mattpocock/sandcastlePublic

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

AI summary: A realistic TypeScript code sandbox environment designed for practical, interactive learning.

Stars
8.2K
+40 today
Forks
889
Watchers
30
Open issues
112
Open PRs
61
Contributors
~12
Commits
1.2K
Branches
110

TypeScriptMITCreated Mar 17, 2026Last push 2d agoLatest release v0.12.0+112 stars this week+424 this month

Quick answers

What is sandcastle?
A realistic TypeScript code sandbox environment designed for practical, interactive learning.
What does sandcastle do?
Sandcastle provides a realistic, browser-based TypeScript sandbox designed to facilitate interactive learning and code experimentation. It closely mirrors a professional development environment, allowing users to write complex TypeScript code and receive immediate, accurate type-checking feedback. The platform is tailored to help developers practice advanced type manipulations, utility types, and generic constraints without needing to configure a local project. By offering a clean, distraction-free interface with instant compiler feedback, it serves as an excellent tool for both teaching and mastering TypeScript concepts.
Who is sandcastle for?
TypeScript developers, technical educators, and students looking for a reliable, realistic environment to practice and demonstrate advanced typing concepts.
How do I get started with sandcastle?
Visit the deployed application to start writing TypeScript code immediately.
How popular is sandcastle on GitHub?
mattpocock/sandcastle has 8,233 stars and 889 forks on GitHub, and gained 112 stars in the last 7 days.
What license does sandcastle use?
mattpocock/sandcastle is released under the MIT license.

Star history

since Jul 29, 2026
02.5K5K7.5KJul 2026Aug 2026Sep 2026Oct 2026
8.2K stars as of Oct 2, 2026. Measured daily since Jul 29, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
SepOctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-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 commits2026-08-02: 0 commits2026-08-03: 0 commits2026-08-04: 0 commits2026-08-05: 0 commits2026-08-06: 0 commits2026-08-07: 0 commits2026-08-08: 0 commits2026-08-09: 0 commits2026-08-10: 0 commits2026-08-11: 0 commits2026-08-12: 0 commits2026-08-13: 0 commits2026-08-14: 0 commits2026-08-15: 0 commits2026-08-16: 0 commits2026-08-17: 0 commits2026-08-18: 0 commits2026-08-19: 0 commits2026-08-20: 0 commits2026-08-21: 0 commits2026-08-22: 0 commits2026-08-23: 0 commits2026-08-24: 0 commits2026-08-25: 0 commits2026-08-26: 0 commits2026-08-27: 0 commits2026-08-28: 0 commits2026-08-29: 0 commits2026-08-30: 0 commits2026-08-31: 0 commits2026-09-01: 0 commits2026-09-02: 0 commits2026-09-03: 0 commits2026-09-04: 0 commits2026-09-05: 0 commits2026-09-06: 0 commits2026-09-07: 0 commits2026-09-08: 0 commits2026-09-09: 0 commits2026-09-10: 0 commits2026-09-11: 0 commits2026-09-12: 0 commits2026-09-13: 0 commits2026-09-14: 0 commits2026-09-15: 0 commits2026-09-16: 0 commits2026-09-17: 0 commits2026-09-18: 0 commits2026-09-19: 0 commits2026-09-20: 0 commits2026-09-21: 0 commits2026-09-22: 0 commits2026-09-23: 0 commits2026-09-24: 0 commits2026-09-25: 0 commits2026-09-26: 0 commits
864 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Very active

    864 commits in 52 weeks

  • Permissive license

    MIT

What sandcastle does

Sandcastle provides a realistic, browser-based TypeScript sandbox designed to facilitate interactive learning and code experimentation. It closely mirrors a professional development environment, allowing users to write complex TypeScript code and receive immediate, accurate type-checking feedback. The platform is tailored to help developers practice advanced type manipulations, utility types, and generic constraints without needing to configure a local project. By offering a clean, distraction-free interface with instant compiler feedback, it serves as an excellent tool for both teaching and mastering TypeScript concepts.

TypeScript developers, technical educators, and students looking for a reliable, realistic environment to practice and demonstrate advanced typing concepts.

  • Browser-Based Sandbox: Allows users to write and execute TypeScript code directly in the browser without local setup.
  • Accurate Type Checking: Utilizes the official TypeScript compiler to provide immediate, realistic error highlighting and feedback.
  • Interactive Learning Environment: Designed specifically to support educational workflows, making it ideal for tutorials and courses.
  • Advanced Type Support: Fully supports complex TypeScript features like generics, conditional types, and mapped types.
  • Distraction-Free Interface: Provides a clean, focused editor to minimize cognitive load while tackling difficult type problems.

Where teams use it

Interactive Tutorials

Instructors embed Sandcastle instances into their educational materials to provide hands-on TypeScript exercises for students.

Rapid Prototyping

Developers use the sandbox to quickly test complex generic type definitions before integrating them into a larger codebase.

Skill Assessment

Companies utilize the environment to conduct technical interviews, asking candidates to solve specific typing challenges live.

Bug Reproduction

Open-source maintainers ask contributors to reproduce TypeScript compiler errors in the sandbox for easier debugging.

Getting started: Visit the deployed application to start writing TypeScript code immediately.

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

Code frequency

additions and deletions
+25.4K-25.4KWeek of 2026-03-15: +10,579 linesWeek of 2026-03-15: -1,536 linesWeek of 2026-03-22: +25,357 linesWeek of 2026-03-22: -12,656 linesWeek of 2026-03-29: +5,061 linesWeek of 2026-03-29: -1,538 linesWeek of 2026-04-05: +6,417 linesWeek of 2026-04-05: -2,727 linesWeek of 2026-04-12: +24,345 linesWeek of 2026-04-12: -9,764 linesWeek of 2026-04-19: +9,814 linesWeek of 2026-04-19: -2,750 linesWeek of 2026-04-26: +874 linesWeek of 2026-04-26: -138 linesWeek of 2026-05-03: +3,926 linesWeek of 2026-05-03: -384 linesWeek of 2026-05-10: +176 linesWeek of 2026-05-10: -171 linesWeek of 2026-05-17: +677 linesWeek of 2026-05-17: -604 linesWeek of 2026-05-24: +15,175 linesWeek of 2026-05-24: -5,960 linesWeek of 2026-05-31: +155 linesWeek of 2026-05-31: -16 linesWeek of 2026-06-07: +43 linesWeek of 2026-06-07: -32 linesWeek of 2026-06-14: +3,957 linesWeek of 2026-06-14: -901 linesWeek of 2026-06-21: +0 linesWeek of 2026-06-21: -0 linesWeek of 2026-06-28: +147 linesWeek of 2026-06-28: -27 linesWeek of 2026-07-05: +0 linesWeek of 2026-07-05: -0 linesWeek of 2026-07-12: +0 linesWeek of 2026-07-12: -0 linesWeek of 2026-07-19: +0 linesWeek of 2026-07-19: -0 linesWeek of 2026-07-26: +0 linesWeek of 2026-07-26: -0 linesWeek of 2026-08-02: +0 linesWeek of 2026-08-02: -0 linesWeek of 2026-08-09: +0 linesWeek of 2026-08-09: -0 linesWeek of 2026-08-16: +0 linesWeek of 2026-08-16: -0 linesWeek of 2026-08-23: +0 linesWeek of 2026-08-23: -0 linesWeek of 2026-08-30: +0 linesWeek of 2026-08-30: -0 linesWeek of 2026-09-06: +0 linesWeek of 2026-09-06: -0 linesWeek of 2026-09-13: +0 linesWeek of 2026-09-13: -0 linesWeek of 2026-09-20: +0 linesWeek of 2026-09-20: -0 linesMar 15, 2026Sep 20, 2026
+106.7K lines added, -39.2K removed over the last year.

Commits per week

last 52 weeks
2360Week 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 commitsWeek of 2026-08-02: 0 commitsWeek of 2026-08-09: 0 commitsWeek of 2026-08-16: 0 commitsWeek of 2026-08-23: 0 commitsWeek of 2026-08-30: 0 commitsWeek of 2026-09-06: 0 commitsWeek of 2026-09-13: 0 commitsWeek of 2026-09-20: 0 commitsSep 28, 2025Sep 20, 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.

    456.7K stars · TypeScript

  • openclaw/openclaw

    The AI that really does things. Any OS. Any Platform. The lobster way. 🦞

    391.3K stars · TypeScript

  • anomalyco/opencode

    The open source coding agent.

    211.7K stars · TypeScript

  • n8n-io/n8n

    Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

    206.7K stars · TypeScript

  • microsoft/vscode

    Visual Studio Code

    193.5K stars · TypeScript

  • firecrawl/firecrawl

    Supercharge your AI agents with data from the web and beyond. Building the library for superintelligence. 🔥

    188.6K stars · TypeScript