termroam
TermRoam · engineering notes

How git works, and how to run AI agents on it

Git tutorials teach commands. This page teaches the model — content-addressed snapshots, one immutable graph, a few movable pointers — because the commands are obvious once you have the model. The second half covers what changes when the committers are AI agents, and the checks that keep finished work from being lost. Everything interactive below is a simulation, safe to click. The § notes carry the precise technical detail.

01

The mental model: snapshots and pointers

Three facts explain most of git.

  1. A commit is a snapshot of your whole project, not a diff. Each commit records a complete tree of your files, plus pointers to the commit(s) it came from, an author, and a message. The diffs you see in tooling are computed on demand between two snapshots.
  2. Commits form an append-only graph. Each commit points at its parent(s); a merge commit has more than one. History is a directed acyclic graph — the DAG. Commits are immutable: you can add new ones and move labels around, but an existing commit never changes. Even "history rewriting" obeys this — see the rebase button below.
  3. A branch is just a name pointing at one commit. Not a copy of the code, not a folder — a 40-character commit ID filed under a name. HEAD is one more pointer that marks where you are, usually by pointing at a branch.

Play with the graph. Watch what each command really moves — most of them move a pointer and nothing else:

commit graph playground
commit = a snapshot   main branch = a movable pointer   HEAD→ = the pointer you're standing on
canvas: drag to pan · pinch or +/− to zoom · ⤢ fit re-centers
Every node is a full snapshot. main and HEAD are just pointers. Try git commit a few times, then branch off, commit on both, and try merge or rebase.
Why this matters: because branches are pointers, they cost nothing. Because commits are immutable, anything you have ever committed is recoverable — after a bad merge, rebase, or reset, the old snapshots still exist, and git reflog lists every position HEAD has ever occupied. The only work git cannot protect is work that was never committed. This matters again in section 6.

precision notes

Snapshots vs. storage. The object model is snapshots; the storage layer is smarter. Loose objects are zlib-compressed full contents; packfiles additionally delta-compress similar objects against each other. Delta compression is invisible at the model level — a design separation worth admiring.

Branch = file, mostly. A loose branch ref is literally a 41-byte file (40 hex chars + newline) at .git/refs/heads/<name>; refs may also be consolidated into .git/packed-refs. Conceptually: one row in a name → commit-ID table.

Parents. Merge commits usually have two parents but git permits N (an "octopus merge"). Root commits have zero.

Detached HEAD is just HEAD pointing directly at a commit instead of at a branch. Nothing is wrong — you're simply standing on a snapshot with no label; commits made here need a branch (or the reflog) to stay findable.

"Append-only" fine print. Unreachable objects are eventually garbage-collected (defaults: reflog entries for unreachable commits expire after 30 days, reachable after 90, then git gc prunes). "Forever" in practice means "months, unless you ask for longer."

02

Content addressing: one hash names everything

The central design idea: every object is named by the hash of its contents. A file's contents hash to a blob ID. A directory listing (filenames + the blob IDs inside it) hashes to a tree ID. A commit (the tree ID + parent commit ID + author + message) hashes to the commit ID. Each level's name contains the names below it — a structure known as a Merkle DAG.

The consequence: a commit ID authenticates the entire history behind it. Change one byte in one file anywhere in the past, and every hash above it changes — the tree, the commit, and every descendant commit. History cannot be quietly edited — only replaced with different history under different IDs. The demo below computes real SHA-1 hashes in your browser:

live hash cascade — edit the file, watch every ID change
your file — app.ts
↓ hash of "blob <size>\0<content>"
blob object
blob 
↓ tree references the blob's ID by name
tree object (the directory)
100644 blob   app.ts
tree 
↓ commit references the tree's ID
commit object
tree 
parent 9c4e21ab…   author you <you@example.com>
message: "feat: compute the answer"

commit 
Type in the file above — even adding a space reshuffles every hash downstream. A commit ID is therefore a fingerprint of everything it contains and everything before it.
precision notes — hashes, SHA-1, and the SHA-256 transition

Exact format. Git hashes "<type> <byte-length>\0<content>". The blob hash above is byte-exact — check it yourself: printf '%s' 'export const answer = 42;' | git hash-object --stdin. Tree and commit objects here use a simplified text serialization (real trees are binary, real commits carry committer + timezone lines); the cascade principle is identical.

SHA-1 vs SHA-256. Historic git uses SHA-1. Collisions have been demonstrated (SHAttered, 2017), so git added collision-detecting SHA-1 and a full SHA-256 object format (git init --object-format=sha256). Content addressing protects integrity against accident and casual tampering; it is not, by itself, a signature — that's what commit signing adds (see the gaps audit below).

Same content, same ID. Two identical files anywhere in history are stored once — deduplication falls out of content addressing for free.

03

The four places your code lives

Every day-to-day git command moves changes between four places. Confusion about git is almost always confusion about which place a change is currently in — and therefore how protected it is.

follow one file through the pipeline
💻 your machine

working tree
files on disk · unprotected

git add

staging (index)
the next commit, assembled

git commit

local repository
the graph · safe here

git push← fetch / pull
☁️ origin — github / your server

shared repository
published · visible to everyone

Click edit file to change app.ts in the working tree.

The staging area is the step people skip explaining: it lets you compose a commit — pick these hunks, not those — instead of committing whatever happens to be on disk. And the last hop is the one that matters most in a team or a fleet: until you push, your commit exists on exactly one machine. Committed ≠ shipped. Pushed to the shared branch = shipped.

precision notes — remotes and the distributed model

Every clone is a full repository — complete history, fully offline-capable. "origin" is not a server role; it's the default nickname for wherever you cloned from. Two clones are peers; convention (and hosting platforms) make one of them the meeting point.

origin/main is your local, read-only memo of where main was on the remote at your last fetch. git fetch updates the memo; git pull = fetch + merge (or rebase) into your branch. Most "git is confusing" moments are the three mains — main here, origin/main the memo, and main on the actual remote — drifting apart.

04

Using git well

Commits are save points

Commit small and often — every coherent step, not every day. A tight history buys you: instant rollback targets, git bisect (binary-search history to find the exact commit that broke something — O(log n) debugging), and archaeology that answers "why is this code like this?" months later via git log and git blame. With AI agents this stops being taste and becomes load-bearing: the commit is the unit of review, revert, and attribution.

The safety nets, in order of reach

Two phrases you will hear

"Rebase before merging." While you worked on a branch, main moved. Rebasing replays your commits on top of the latest main (see the rebase button in section 1), so your changes apply to current code and the eventual merge is trivial — often a fast-forward. Safe rule: rebase freely on branches only you have; never rebase commits someone else has already pulled.

"Fast-forward." When the target branch has not moved since you branched, there is nothing to reconcile — git just slides the pointer forward. No merge commit, because the histories never actually diverged.

Quick check

A teammate hard-reset a shared branch and your pushed commit "disappeared." Is your work gone?

Commits are immutable; moving a pointer never deletes one. git reflog on any machine that had the commit — or git branch rescue <id> straight from the ID — re-anchors it. Unreferenced commits survive for weeks before garbage collection. The deeper fix is preventing shared-branch rewrites at the remote (branch protection — see the gaps audit).
05

Worktrees — and the trap almost nobody warns you about

A worktree is an additional folder on disk attached to the same repository — same object store, same branches, separate working files. They're how you work on two branches at once without stashing, and how AI agents get isolated sandboxes without the cost of full clones.

The trap: git allows each branch to be checked out in only ONE worktree at a time. The rule is sound — two folders both "owning" a branch pointer would corrupt each other's view. The corollary: a forgotten worktree parked on main silently blocks every other checkout of main, including the automated merge job that lands finished work. If that job's error handling is quiet, completed work strands on a side branch while every dashboard reports success. This exact failure hit our own pipeline: twelve finished commits sat unmerged behind one stale worktree until an audit found them.

reproduce it

~/project (primary checkout)

on feat/big-refactor
feature finished — needs to merge to main

~/worktrees/experiment (forgotten last week)

on main
someone parked this here and moved on
The primary checkout needs to get onto main to merge. Try it.

Who worktrees are for — and who they are not for

Three mappings to keep straight, because they are easy to blur:

Managing worktrees: creation, lifecycle, cleanup

Worktrees never create themselves and never clean themselves up. Git gives you exactly these controls and no lifecycle beyond them:

the complete management surface
git worktree add ../fix-login main   # create: new folder, checks out a branch
git worktree list                    # inventory — run this before anything surprises you
git worktree lock ../agent-7 --reason "agent 7 owns this"   # mark as in-use
git worktree remove ../fix-login     # delete when the work has landed
git worktree prune                   # clean metadata for folders already deleted by hand

The rule that prevents every worktree problem on this page: whoever creates a worktree owns its funeral. The lifecycle is create → work → land the work (merge or push) → remove. Nothing expires them for you — a forgotten worktree sits forever, holding its branch hostage and accumulating un-narrated changes. If a tool created it (agent orchestrators, including TermRoam, create per-agent worktrees programmatically), the tool must also remove it — and sweep for the ones it failed to.

If an agent dies or resets, is its worktree stranded?

The disk state survives perfectly — files, branch, uncommitted edits; git loses nothing. What dies with the agent is the intent: a fresh agent (or you) finds a dirty worktree with no narrative about what the edits were for. The fix is not smarter agents — it is externalizing memory into artifacts. The task spec records what is done and what is next; commit messages record why; one-commit-per-step keeps the uncommitted window tiny. Under that discipline, resetting an agent is safe: any replacement reads the spec, sees step 6 checked and step 7 not, and continues. Without it, every reset risks stranding mystery work — which is why a sweep that commits dirty, ownerless worktrees to pushed rescue branches is the backstop worth automating (TermRoam's watcher does exactly this).

Are worktrees part of the git workflow?

No — the core loop (edit → add → commit → push → merge) never requires one, and you can use git for a career without the command. They are a concurrency tool for a single machine. What has changed is that agent fleets promote them from occasional convenience to load-bearing primitive — a sandbox per writer — and vanilla git provides no management for that role: no expiry, no ownership, no orphan detection. That gap is what your orchestration layer has to fill, and it is why several of the guards in section 8 are really just worktree lifecycle management.

The immediate fix is one command — git switch --detach in the offending worktree drops the branch name without touching a single file, freeing main for checkout elsewhere. The durable fix is refusing to depend on a human noticing — that's section 7.

06

What changes when the committers are AI agents

Git's guarantees were designed for humans who notice things. Agents are fast, tireless, parallel — and they do not notice things. Run several against one repository and you surface failure modes human teams rarely hit:

Anatomy of a collision (the one we hit most)

The reason shared-checkout collisions are so damaging is that git raises no error — every command each agent runs is individually valid. Only the combined result is wrong. A typical sequence:

collision timeline — two agents, one checkout
14:02  Agent A starts refactoring auth.ts — half-finished edits sit uncommitted in the tree
14:03  Agent B (same folder) runs git switch feat/search — git swaps every file to that branch's snapshot and carries A's uncommitted edits along
14:05  Agent B runs git add -A && git commit — A's stray half-edits are now sealed inside B's "add search" commit
14:07  Agent A resumes; the files no longer match what it was editing, so it "helpfully repairs" them — reverting work B just committed
14:09  both agents report success. The history is a shuffle of two tasks; neither is complete; no error was ever raised

Variants of the same disease, all from our own logs: a branch switch yanking files out from under a process that was reading them (worse when a dev server runs from that working tree — it silently starts serving the other branch's code); two package installs racing on one lockfile; one agent running the test suite while another mutates the fixtures it's reading. Different symptoms, one cause: two writers, one working tree.

The fix is structural, not behavioral: give every concurrent writer its own working tree — a separate worktree or a separate clone — or don't let them be concurrent at all (queue them). Locks made of prose do not survive contact with a machine that reads instructions statistically.

The core insight: every git guarantee teams enforce by convention — don't force-push, rebase before merging, clean up branches, confirm it landed — must instead be enforced by a machine that checks invariants. Convention is violated at machine speed.
07

Working with coding agents: the method

The material above condenses into an operating loop that works with any capable coding agent. Each step notes how TermRoam automates it; the method stands on its own.

Spec the task as a checklist of commit-sized steps.Imperative verb, one outcome, acceptance criteria per step ("Verify the test covers both branches"). If a step can't be verified, it isn't a step yet. With TermRoam: specs are PRD files, machine-reviewed by a validator before anything is allowed to run.
Pick the lane before launching.Default to main — no merge step exists to fail. Take a feature branch only when a bad commit would hurt others for more than a few minutes (schema migrations, prompt overhauls) — and know that you now own a merge step and its verification. With TermRoam: the spec's branch field decides, and feature branches automatically get the guarded auto-merge.
Clear the runway: one writer per repo.Parallel agents get separate repos or separate worktrees — never a shared checkout. Queue additional work behind the running agent instead of beside it. With TermRoam: the launch API refuses a second loop on an occupied repo and chains the new spec onto the running one.
Grant full autonomy inside a hard boundary.Permission prompts don't scale to overnight work; dedicated machines and worktrees do. The sandbox is the boundary — plus standing rules committed to the repo (copy the block below). With TermRoam: the box is the sandbox; agent CLIs arrive preconfigured with the rules baked in.
One step → one commit → push. Every time.Land each step before starting the next. Uncommitted work is the only work git cannot protect, so minimize how long any work stays uncommitted. With TermRoam: loops commit and push every iteration by construction.
Interrupt between steps, never mid-step.Killing an agent mid-step orphans exactly the work git can't save. Pause at the commit boundary. With TermRoam: a pause API stops loops cleanly — and the rescue sweep catches whatever a hard kill leaves behind anyway.
Verify the landing — status is a claim.After every "done": the branch's commits are on origin/main, the primary checkout is back on main, the tree is clean. With TermRoam: the watcher proves this continuously and badges violations UNMERGED.
Review by commit, not by pile.Commit-sized steps make review linear: read each commit like a paragraph. Reverting one bad step is trivial; un-reviewing a merged pile is not. With TermRoam: the Execute board shows the step-by-step timeline per task.
Sweep on a schedule; automate on the second find.Dirty worktrees, "complete" tasks with unmerged branches, stale rescue branches. Anything a sweep finds twice becomes a machine's job. With TermRoam: the merge-debt and rescue sweeps are already machines.

Standing rules to give any agent

Commit these into whatever instruction file your agent reads. They are the distilled, non-negotiable subset of everything above (and they're what TermRoam boxes ship by default):

AGENT-RULES.md — copy freely
1. One task step = one commit, pushed immediately.
2. Never force-push. Never rewrite pushed history. Never use --no-verify.
3. Never kill servers, tmux sessions, or processes you did not start.
4. On merge conflict: STOP and report. Never resolve by discarding either side.
5. Never commit secrets or .env files.
6. "Done" requires proof: `git log origin/main..HEAD` prints nothing, and the tests named in the step pass.

How the discipline scales

08

How TermRoam implements this

TermRoam is a dedicated always-on server for coding agents. Its execute workflow implements the method above — and builds most of TermRoam itself with it:

The pipeline, end to end

step through it — or tap any stage

1 · Spec (PRD)

Written & machine-reviewed

2 · Loop

One agent, one repo, one step at a time

guard: one-writer lock

3 · Commit + push

Each step lands as one pushed commit

guard: typecheck gate + CI

4 · Auto-merge

Guarded merge to main

guard: main-detach + conflict block

5 · Verify

Invariants checked forever, not once

guard: merge-debt + rescue sweep
This is the system that writes most of TermRoam's own code — the same pipeline every customer box runs.
09

What we haven't covered yet (an audit we maintain)

This audit tracks what we actually run. Researching this page closed three rows. Green is machine-enforced, amber is partial, red is open:

covered Single-writer serialization & merge queue

One live loop per repo, enforced at launch; queued specs chain serially. Functionally a merge queue built into the orchestrator.

covered Merge-debt detection ("complete ≠ merged")

Watcher events plus an UNMERGED badge whenever a finished task's commits are absent from origin/main. Born from a real incident: twelve commits stranded behind one stale worktree holding main.

covered Orphaned-work rescue

Dirty worktrees with no live loop are auto-committed to rescue/* branches and pushed. A killed agent's worst case is a pushed branch awaiting review.

covered Independent CI gate

Closed while writing this page (July 2026). Every push now triggers a fresh-clone install, full typecheck, and tests on neutral compute — the check that catches undeclared dependencies and machine-local state by construction. Test coverage in CI starts with the pure packages and is expanding to the box-dependent suites as they're made hermetic.

covered Pinned releases & instant rollback

Closed while writing this page. Every verified deploy is tagged automatically; rollback is "deploy the tag" instead of archaeology under pressure.

partial Secret scanning

Today: gitleaks scans full history on every push in CI (added with the CI gate).

Remaining: a pre-push hook in the agent wrapper, so a secret is caught before it enters immutable history rather than right after. Detection exists; prevention is the last mile.

partial Force-push & history protection

Today: "never force-push" is a standing rule in every agent's instructions — convention, not a wall.

Remaining: remote branch-protection (deny force-push/deletion on main). Attempted; currently blocked by an API-token permission — a credential change, not an engineering task.

partial Rescue-branch & worktree lifecycle

Sweeps exist; nothing expires their output yet. Planned: surface rescues for N days, then archive as tags and delete branches, so a year of operation doesn't bury signal in noise.

partial State that lives outside git

Code is remote-backed and recoverable from any clone; databases and queue state replicate off-box next (Litestream). Until then, git's safety net has a hole shaped like everything that isn't code.

missing Commit provenance & signing

Content addressing gives integrity, not identity: any process can write any name into a commit. Per-agent SSH signing keys, verified at the remote, are the fix — and they matter more with every agent added.

The rows that silently destroy finished work went green first; CI and rollback pins went green while writing this page. What remains is identity and disaster recovery. If you are building your own setup, work the list top to bottom. TermRoam ships with the green rows in place.