Implements all 7 commands: new, save, push, list, open, stop, rm. Uses Commander for CLI parsing, Anthropic SDK for AI commit messages and merge conflict resolution, and Apple container for VMs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.1 KiB
sandlot
A CLI for branch-based development using git worktrees and Apple containers. Each branch gets its own worktree and isolated VM. Commits are auto-summarized by Claude. Merging cleans everything up.
Concepts
Sandlot is a thin workflow layer over three things: git worktrees, Apple containers, and the Claude API. The idea is that spinning up a branch should give you a fully isolated environment — filesystem and runtime — with zero setup. When you're done, you merge and everything tears down.
A sandlot session is a (worktree, VM) pair tied to a branch. Sessions are created with sandlot <branch> and destroyed on merge.
Tech Stack
- Bun runtime
- Commander for CLI parsing
- Apple container for VMs
Setup
Prerequisites
Install
git clone https://github.com/your/sandlot
cd sandlot
bun install
bun link
This makes sandlot available globally.
Configure
Set your Anthropic API key:
export ANTHROPIC_API_KEY=sk-ant-...
Add to your shell profile (.zshrc, .bashrc, etc.) to persist it.
CLI
sandlot new <branch>
Create a new session. This:
- Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD
- Creates a git worktree at
.sandlot/<branch>/(relative to the repo root) - Boots an Apple container VM mapped to that worktree
- Drops you into the VM shell
$ sandlot new fix-POST
Creating worktree at .sandlot/fix-POST/
Booting VM...
root@fix-POST:~#
sandlot save [message]
Stage and commit all changes. If a message is provided, use it. If not, generate one with Claude.
- Runs
git add .in the worktree - If no message provided: diffs staged changes against the last commit, sends the diff to Claude API (claude-sonnet-4-20250514) to generate a commit message
- Commits with the message
- Pushes the branch to origin
If there are no changes, does nothing.
The AI-generated commit message is a single-line summary (≤72 chars) followed by an optional body with more detail if the diff is substantial.
$ sandlot save
Staged 3 files
Commit: Fix POST handler to validate request body before processing
Pushed fix-POST → origin/fix-POST
$ sandlot save "wip: rough cut of validation"
Staged 3 files
Commit: wip: rough cut of validation
Pushed fix-POST → origin/fix-POST
sandlot push <target>
Push the current session's branch into <target>, then tear everything down.
- Checks out
<target>in the main working tree - Merges the session branch (fast-forward if possible, merge commit otherwise)
- Pushes
<target>to origin - Stops and removes the VM
- Removes the worktree
- Deletes the local branch
- Deletes the remote branch
If there are uncommitted changes in the worktree, prompts to sandlot save first.
If the merge has conflicts:
- Collects all conflicted files
- For each file, sends the full conflict diff (ours, theirs, and base) to the Claude API
- Claude resolves the conflict and returns the merged file
- Writes the resolved file and stages it
- Shows a summary of what Claude chose and why
- Prompts for confirmation before committing the merge
If you reject Claude's resolution, drops you into the main working tree to resolve manually. Run sandlot push <target> again to complete cleanup.
$ sandlot push main
Pushing fix-POST → main...
Pushed main → origin/main
Stopped VM fix-POST
Removed worktree .sandlot/fix-POST/
Deleted branch fix-POST (local + remote)
With conflicts:
$ sandlot push main
Pushing fix-POST → main...
2 conflicts found. Resolving with Claude...
src/handlers/post.ts
✓ Kept the new validation logic from fix-POST,
preserved the logging added in main
src/routes.ts
✓ Combined route definitions from both branches
Accept Claude's resolutions? [Y/n] y
Committed merge
Pushed main → origin/main
Stopped VM fix-POST
Removed worktree .sandlot/fix-POST/
Deleted branch fix-POST (local + remote)
sandlot list
Show all active sessions.
$ sandlot list
BRANCH VM STATUS WORKTREE
fix-POST running .sandlot/fix-POST/
refactor-auth stopped .sandlot/refactor-auth/
sandlot open <branch>
Re-enter an existing session's VM. If the VM is stopped, boots it first.
$ sandlot open fix-POST
Booting VM...
root@fix-POST:~#
sandlot stop <branch>
Stop a session's VM without destroying it. The worktree and branch remain.
sandlot rm <branch>
Tear down a session without merging. Stops the VM, removes the worktree, deletes the local branch. Does not touch the remote branch.
Configuration
Optional sandlot.json at the repo root:
{
"vm": {
"cpus": 4,
"memory": "8GB",
"image": "ubuntu:24.04",
"mounts": {
"/path/to/shared/deps": "/deps"
}
},
"ai": {
"model": "claude-sonnet-4-20250514"
}
}
State
Sandlot tracks sessions in .sandlot/state.json at the repo root:
{
"sessions": {
"fix-POST": {
"branch": "fix-POST",
"worktree": ".sandlot/fix-POST",
"vm_id": "container-abc123",
"created_at": "2026-02-16T10:30:00Z",
"status": "running"
}
}
}
.sandlot/ should be added to .gitignore.
Edge Cases
- Nested sandlot calls:
sandlot saveandsandlot pushdetect the current session from the working directory or aSANDLOT_BRANCHenv var set when entering the VM. - Stale VMs: If a VM crashes,
sandlot openshould detect the dead VM and reboot it. - Multiple repos: State is per-repo. No global daemon.
- Branch name conflicts: If
.sandlot/<branch>/already exists as a directory but the session state is missing, prompt to clean up or recover.
Non-Goals
- Not a CI/CD tool. No pipelines, no test runners.
- Not a replacement for git. All git state lives in the real repo. Sandlot is sugar on top.
- No multi-user collaboration features. This is a single-developer workflow tool.