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.
Three facts explain most of git.
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:
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.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."
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:
blob …100644 blob … app.ts tree …
tree … parent 9c4e21ab… author you <you@example.com> message: "feat: compute the answer" commit …
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.
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.
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.
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.
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.
git restore <file> — discard uncommitted edits to a file. (The one net that destroys rather than recovers — it throws away work that was never committed.)git revert <commit> — undo a commit by adding a new commit that inverts it. Safe on shared branches; history stays truthful.git reset — move your branch pointer to an older commit. Fine for local, unpushed work; on a shared branch it rewrites history others built on, which is why force-pushing shared branches is banned in serious teams.git reflog — the flight recorder: every position HEAD has occupied, including positions "lost" to resets and rebases. When someone says git ate their commit, the reflog almost always disagrees."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.
A teammate hard-reset a shared branch and your pushed commit "disappeared." Is your work gone?
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).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.
Three mappings to keep straight, because they are easy to blur:
Worktrees never create themselves and never clean themselves up. Git gives you exactly these controls and no lifecycle beyond them:
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.
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).
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.
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:
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:
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 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.
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):
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.
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:
Written & machine-reviewed
One agent, one repo, one step at a time
Each step lands as one pushed commit
Guarded merge to main
Invariants checked forever, not once
This audit tracks what we actually run. Researching this page closed three rows. Green is machine-enforced, amber is partial, red is open:
One live loop per repo, enforced at launch; queued specs chain serially. Functionally a merge queue built into the orchestrator.
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.
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.
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.
Closed while writing this page. Every verified deploy is tagged automatically; rollback is "deploy the tag" instead of archaeology under pressure.
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.
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.
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.
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.
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.