Companion to architecture.md (the why). This doc is the
how: file/git handling during create, and the full checkpoint
capture/restore mechanics, with code pointers. Source of truth:
packages/sandbox-docker/src/create.ts, in-box-git.ts,
git-worktree.ts, checkpoint.ts.
- Detect repos —
detectGitRepos(workspace)(git-worktree.ts) scans for a.gitdirectory at the workspace root and at every 1st-level subdirectory (monorepo case). Worktree-form.gitfiles are skipped (rare and weird). - Pick branches on the host — for each detected repo,
pickFreshBranch(hostMainRepo, "agentbox/<box-name>")bumps-2,-3, … until git reports no such branch. Runs against the host's refs beforedocker runso the branch name is committed to theBoxRecordregardless of whether the in-containergit worktree addsucceeds. - Capture carry-over (host) —
collectRepoCarryOver(...)inin-box-git.ts:git -C <repo> stash createproduces a stash commit without touching the working tree or stash list. The commit lands in the host's.git/object DB, which is bind-mounted into the container — so the in-box worktree canstash apply <sha>against it.git -C <repo> ls-files --others --exclude-standard -zenumerates the untracked paths.
- Bind-mount
.git—runBoxbinds each<hostMainRepo>/.gitat its identical absolute host path inside the container, RW. Worktree pointer files (<wt>/.git) and the back-reference at<main>/.git/worktrees/<name>/gitdircontain absolute paths — both sides must resolve to the same path for git to function symmetrically. - Seed
/workspace—seedWorkspace(...)inin-box-git.tsruns as uservscodeinside the container:Then if a stash SHA was captured:git -C <hostMainRepo> worktree add -b agentbox/<name>[--<sub>] <containerPath> HEAD git -C <hostMainRepo> config extensions.worktreeConfig true git -C <containerPath> config --worktree commit.gpgsign falsegit -C <containerPath> stash apply --index <sha>(falls back to apply-without-index on conflict). Then if untracked files exist: tar them in host-side anddocker exec -i tar -x -C <containerPath>. - No host worktree dir. The worktree's working tree lives only in the
container's writable layer. The host's
.git/worktrees/<name>/gitdirpoints to the container-only/workspacepath — cosmetic ingit worktree liston the host, otherwise inert (git pushdoesn't need a working tree). - Push/pull go through the relay (
packages/relay/src/server.ts):git.pushrunsgit -C <hostMainRepo> push <remote> <branch>on host with user creds (refs are up-to-date in the shared.git);git.pullin the in-boxagentbox-ctl git pullfirst calls a host-sidegit.fetchRPC, then runs a localgit mergein/workspace(no creds needed).
- Plain (no
--host-snapshot):seedWorkspaceFromDir({ container, hostSource: workspace })—tar -C <workspace> -cf - .host-side, piped intodocker exec -i tar -C /workspace -xf -as uid:gid 1000:1000 so extracted files land owned byvscode. --host-snapshot:cp -cAPFS clone of the workspace into~/.agentbox/snapshots/<id>/first, then the same tar pipe from the clone. Stabilizes the source bytes against host edits during create.- Checkpoint restore (
--snapshot <ref>):seedWorkspaceis skipped entirely — the checkpoint Docker image already has/workspacepopulated.
- All writes go to the container's writable layer (the same place Docker layers any in-container write that isn't a volume).
node_modules,.next,target,.venv— all land there. Persists across pause/stop/start; wiped ondestroy.- The host filesystem is never written by the box directly.
agentbox openrsyncs/workspace→~/.agentbox/boxes/<id>/workspaceon demand. --with-envadditionally copies host.env*/secrets.toml/agentbox.yaml-style files (DEFAULT_ENV_PATTERNS) into/workspaceafter seeding, bypassing gitignore — the reverse ofagentbox download env.
Net effect: the agent gets a full, writable copy of the repo on its own branch, with the user's uncommitted work carried over, fully isolated from the host checkout and host filesystem.
Purpose: let a new box start warm (deps installed, project built) instead of
cold, without baking anything into the base image. Code: checkpoint.ts
(capture + resolve) and the restore path in create.ts.
- One Docker image tag per checkpoint:
agentbox-ckpt-<sha1-16(projectRoot)>:<name>(checkpointImageTag, deterministic from the project root — samehashProjectPaththe per-project config dir uses). - Host-side, only metadata:
~/.agentbox/checkpoints/<projectHash>/<name>/manifest.json(schema: 2,type,image,parents,base,sourceBox*,createdAt). The captured filesystem is never on the host — it's a regular Docker image. - Naming is monotonic per box-name (
computeNextCheckpointNameis max+1; gaps from deleted checkpoints never recycled).
- Pre-commit cleanup —
docker exec --user root <ctr> /usr/local/bin/agentbox-checkpoint-cleanup(script body inpackages/sandbox-docker/scripts/agentbox-checkpoint-cleanup, baked into the image at build time):Caches underapt-get clean rm -rf /var/lib/apt/lists/* rm -rf /tmp/* /var/tmp/* truncate -s0 /var/log/**/*.log : > /root/.bash_history /home/vscode/.bash_history~/.npm/~/.cacheand/var/lib/dockerare intentionally kept (warm state worth carrying). Best-effort: every step2>/dev/null || true; cleanup failure never blocks the commit. - Type selection —
--merged, or auto when the source box's chain depth>= checkpoint.maxLayers(default 3, caps image-layer growth). - Layered (default):
docker commit <ctr> <ckpt-tag>. New layer on top of the box's current image; lineage is implicit in Docker image history.parentsin the manifest tracks the source box'scheckpointSource.chainfor display + the auto-flatten threshold. - Flattened (
--merged/ auto): commit to an intermediate tag, thendocker create --name <tmp> <intermediate-tag> sleep 0docker export <tmp> > <scratch>/rootfs.tardocker image inspect <intermediate-tag>forConfig.Env / Cmd / Entrypoint / WorkingDir / User / ExposedPorts(everythingdocker exportdiscards)- Write a tiny
Dockerfile.flatten:FROM scratch ADD rootfs.tar / ENV ... WORKDIR ... USER ... EXPOSE ... ENTRYPOINT ... CMD ...
docker build -t <ckpt-tag> -f Dockerfile.flatten <scratch>- Remove the intermediate tag, scratch dir, and the throwaway container.
The resulting image is a single ADD layer;
parents: [](self-contained).
resolveCheckpoint(projectRoot, ref)reads the manifest and returns the image tag plus lineage.create.tspasses the tag torunBoxas the base image and skipsseedWorkspaceentirely.- Fresh per-box worktree (not the manifest's). The manifest records the
source box's branch + worktree path, but reusing them verbatim is unsafe:
every box from one checkpoint would share a single branch + index (commits
clobber each other;
index.lockfights), and once the source box is destroyed its host.git/worktrees/<name>metadata is pruned, leaving the baked/workspace/.gitgitfile dangling (fatal: not a git repository). So restore allocates a fresh, uniqueagentbox/<box-name>branch (host-sidepickFreshBranch, beforedocker run, like the non-checkpoint path) and, afterdocker run,regenerateRestoredWorktrees(in-box-git.ts) renames the baked content dir to the fresh path (an O(1) in-container rename), mints a fresh branch at the host base ref, authors fresh.git/worktrees/<fresh>metadata, repoints the gitfile, andgit reset --hard HEADso the box starts clean at the host base ref (same git state as a fresh create). The baked tracked tree was the source box's possibly-stale/divergent branch, so its deviations are dropped; the gitignored warm artifacts (node_modules,.next, build caches) are untouched by the reset — that warm state is the checkpoint's value.resyncWorkspaceFromHostthen overlays the host's current uncommitted/untracked work, matching the non-checkpoint carry-over. - Cloud parallel (vercel/daytona/e2b/hetzner). Cloud has no worktrees and
can't bind-mount the host
.git, but the same hazard applies: the snapshot's/workspace/.gitis the source box's per-box branch.createCloudProvider's create re-seeds in overlay mode (seedCloudWorkspace({ overlay: true }),packages/sandbox-cloud/src/workspace-seed.ts): it keeps the snapshot's gitignored warm tree, ships only the missing host commits (checkpointTip..hostTarget) as a git bundle (full-clone.gitswap as fallback when diverged), thengit checkout -f -B agentbox/<new-box>+git reset --hardto the host base ref and replays the host stash + untracked. Carry-over conflicts are resolved box-wins and returned asCreatedBox.resyncso the CLI injects the same conflict warning into the agent as docker. The cloud analogue ofregenerateRestoredWorktrees+resyncWorkspaceFromHost. So a cloud box from a checkpoint lands on a fresh per-box branch with the host's current files, not the frozen source branch. BoxRecord.checkpointImagemirrorsrecord.imagefor plain-vs-checkpoint disambiguation (used byprune --allto allowlist still-referenced checkpoint tags).BoxRecord.checkpointSource = { ref, type, chain }carries the lineage forinspect/statusand the auto-flatten depth count.
- CLI:
agentbox checkpoint create <box> [--merged] [--set-default],agentbox checkpoint ls(the default — bareagentbox checkpointoragentbox checkpointslists),agentbox checkpoint rm <ref>(deletes the manifest and the image tag). - Capture/restore is host-side. An in-box agent triggers it through the
relay:
agentbox-ctl checkpoint→/rpc checkpoint.create→ the relay spawns the hostagentbox checkpoint createCLI (AGENTBOX_CLI_ENTRY) — no host creds leak into the box, reusing the existing relay channel (same asagentbox-ctl git).
--host-snapshot (config box.hostSnapshot) is a per-box APFS clone of the
host workspace used only as a stable source for the create-time tar pipe
(non-git case). It cannot carry box-side state — orthogonal to checkpoints.
agentbox destroyremoves the container, per-box volumes (claude-config if isolated, vscode-server, cursor-server, dockerd unless shared), the per-box host snapshot dir, and the per-box run dir under~/.agentbox/boxes/<id>/. Per-box checkpoint images stay (they're cross-box project assets).agentbox prune --allreaps orphan containers/volumes, the per-box snapshot dirs, andagentbox-ckpt-*image tags not referenced by any survivingBoxRecord.checkpointImageand not referenced by any project's manifests on disk. Shared volumes (agentbox-claude-config,agentbox-{vscode,cursor}-extensions,agentbox-docker-cache) are allowlisted unconditionally.