# 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](https://github.com/apple/container), 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 ` and destroyed on merge. ## Tech Stack - [Bun](https://bun.sh) runtime - [Commander](https://github.com/tj/commander.js) for CLI parsing - [Apple container](https://github.com/apple/container) for VMs ## Setup ### Prerequisites - macOS on Apple Silicon - [Bun](https://bun.sh) installed - Git installed - [container](https://github.com/apple/container) installed and available on PATH ### Install ```bash git clone https://github.com/your/sandlot cd sandlot bun install bun link ``` This makes `sandlot` available globally. ### Configure Set your Anthropic API key: ```bash export ANTHROPIC_API_KEY=sk-ant-... ``` Add to your shell profile (`.zshrc`, `.bashrc`, etc.) to persist it. ## CLI ### `sandlot new ` Create a new session. This: 1. Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD 2. Creates a git worktree at `.sandlot//` (relative to the repo root) 3. Boots an Apple container VM mapped to that worktree 4. 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. 1. Runs `git add .` in the worktree 2. 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 3. Commits with the message 4. 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 ` Push the current session's branch into ``, then tear everything down. 1. Checks out `` in the main working tree 2. Merges the session branch (fast-forward if possible, merge commit otherwise) 3. Pushes `` to origin 4. Stops and removes the VM 5. Removes the worktree 6. Deletes the local branch 7. Deletes the remote branch If there are uncommitted changes in the worktree, prompts to `sandlot save` first. If the merge has conflicts: 1. Collects all conflicted files 2. For each file, sends the full conflict diff (ours, theirs, and base) to the Claude API 3. Claude resolves the conflict and returns the merged file 4. Writes the resolved file and stages it 5. Shows a summary of what Claude chose and why 6. 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 ` 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 ` 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 ` Stop a session's VM without destroying it. The worktree and branch remain. ### `sandlot rm ` 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: ```json { "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: ```json { "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 save` and `sandlot push` detect the current session from the working directory or a `SANDLOT_BRANCH` env var set when entering the VM. - **Stale VMs**: If a VM crashes, `sandlot open` should detect the dead VM and reboot it. - **Multiple repos**: State is per-repo. No global daemon. - **Branch name conflicts**: If `.sandlot//` 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.