Chapter 3 of 18

Version control

Commits, branches, diffs and pull requests: the undo button and the paper trail for everything an agent does.

An agent can change forty files before you have finished reading its first sentence, and some of those changes will be wrong. It will tell you they are fine either way. Version control is what makes that survivable: every step is recorded, you can see exactly what happened, and you can go back.

Git, the tool nearly everyone uses for this, gives you three things here. It is the safety net, because work saved into its history, which is called committing, is very hard to lose. It is the instrument for reading what an agent really did, as opposed to what it says it did. And it is the paper trail: a history of small commits with honest messages shows that a person was steering, and you can point at it later.

Most of git only ever adds to the history, so most of it is safe. A short list of commands can destroy work: reset --hard, checkout -- ., restore ., clean, stash drop and force push. This chapter makes you operate each of them on a real model of git, then shows you how to stop an agent running them when it is in a hurry to finish.

Version control

Also called VCS, Source control

A system that keeps every saved version of a project, with who changed what and when, so you can compare any two versions or go back to one. Git is the one this course uses, and the one you will keep meeting.

An agent can be confidently wrong across a whole project in a minute. Version control is what makes that cheap: you can see precisely what it did, throw away what you don't want, and return to the last state that worked. Without it, every agent mistake is a manual repair job.

Say it to your agent

“Before you change anything, run git status and tell me if the working tree is clean. If it isn't, stop and ask me. Commit each finished step, so that I can go back to any of them.”

Repository

Also called Repo

One project under version control: your files, plus a hidden folder called .git that holds the entire history. Run git init in a folder and it becomes a repository. Delete .git and the history is gone.

The whole history sits on your machine, in a folder an agent can reach. Reading it is how an agent catches up at the start of a session, and damaging it is one of the few ways it can really hurt you, which is why agent tools treat that folder as special.

Say it to your agent

“Run git init, add a .gitignore covering node_modules and .env, and make a first commit called "chore: initial commit" before you write any code, so every change after this can be seen and undone.”

Everything else in this chapter is built out of one thing.

Commit

A saved snapshot of the whole project, with a message, an author, a time, and a link to the commit before it. Its name is a hash: a long fingerprint worked out from all of that, so identical contents always get the same name.

Committed work is the only agent work that is safe, because anything uncommitted can be overwritten by one command. And since each hash includes the hash before it, nobody can alter the past without every later hash changing, so a history can serve as evidence.

Say it to your agent

“Commit now, with only the files for this change: git add src/cart.ts src/cart.test.ts, then git commit. Show me the hash and the output of git show --stat HEAD.”

Diff

A list of exactly what changed between two versions: the lines removed, the lines added, and a few around them for context. Removed lines start with a minus sign and added lines with a plus.

An agent's summary is a story it tells afterwards. The diff is the thing itself, and it shows what the summary left out: a deleted test, a changed setting, a file you never asked it to touch. Learning to read one is the first skill of supervising an agent.

Say it to your agent

“Show me git diff --stat main...HEAD first, then the full diff. If you reformatted anything, split it out: I want one commit with spacing changes only, and another with the real change.”

Staging area

Also called Index, The cache

A holding area between your files and the next commit. git add puts a copy of a file there, exactly as it is at that moment, and git commit records only what is there. This lets you commit one change while others wait.

Agents like sweeping commands. git add -A stages whatever is in the folder, including the .env it just created and the debug file it forgot about. Naming each file keeps the commit to what you meant, and it is much the easiest way to keep a secret out of history.

Say it to your agent

“Don't use git add -A or git add . in this repo. Stage files by name, then run git diff --staged and check that nothing is in there I didn't ask for before you commit.”

So far the history is a single line. The move that makes agents safe to run is letting it fork.

Branch

A name that points at one commit, and moves forward each time you commit on it. It lets you work on one thing without touching the main line. HEAD is git's marker for which branch you are on.

A branch is how you give an agent room to be wrong. If the work goes badly, you switch back and delete the branch, and the main line never knew. Since branches cost almost nothing, one for each task is a sensible default.

Say it to your agent

“Create a branch called fix/cart-rounding from main before you change anything. Work only on that branch. Don't merge it, and don't push to main.”

Main branch

Also called main, master, Default branch, Trunk

The branch everyone treats as the real version of the project. Work happens elsewhere and joins it when it is ready. The name is only a convention: many older projects call it master.

An agent working on main is one bad commit away from breaking what everyone depends on. Don't assume your tools protect it. Claude Code's auto mode, as of November 2026, allows pushes to the default branch (sending your commits to a shared server, coming shortly) unless your own rules or the server stop them.

Say it to your agent

“Never commit directly to main. If git status says you are on main, stop, run git switch -c with a name for this task, and carry on there.”

Merge

Bringing one branch's work into another. If only one side has moved, git simply slides the branch forward. If both have, it finds the commit they share, combines the two sets of changes, and makes a new commit with two parents.

Once several agents are working, the merge is where their work meets, and where two individually correct changes can stop being correct together. Merge a branch that has passed its checks, then run those checks again on the result.

Say it to your agent

“Merge feature/search into main with --no-ff so the branch stays visible in the history, then run bun test on the result before pushing. If anything conflicts, stop and show me.”

Merge conflict

What happens when two branches changed the same lines. Git can't choose, so it stops and writes both versions into the file between markers, and someone has to decide what the file should really say.

Agents resolve conflicts quickly and often pick wrong, because making the markers go away is easy and looks finished. So ask what each side was trying to do. And the opposite danger is real: a merge with no conflict can still be broken, since git compares text and knows nothing about code.

Say it to your agent

“Run git config merge.conflictStyle zdiff3, then read both commits with git log -p and tell me what each side was trying to do. Keep both intents, and run the tests. If you can't tell which is right, stop and ask. Don't guess.”

Rebase

Moving your commits so they start from a newer point, by replaying them one at a time. The result looks as if you began today and worked in a straight line. But each replayed commit is a new commit, with a new hash.

It lets an agent's messy branch become a clean series before anyone reads it. The risk comes from the same place as the benefit: it rewrites history, so it is only safe on commits nobody else has. An agent won't know who else has them unless you say.

Say it to your agent

“Rebase this branch onto the latest main, and squash your fixups into small, ordered commits that each pass bun test. Only I am using this branch, so that is safe. Don't rebase anything already merged or pulled by anyone else.”

Everything so far has happened on one machine.

Remote

Also called origin

Another copy of the repository, usually on a server, which yours knows by name. The usual name is origin. Your machine also keeps its last-known note of where the server's branches were, called origin/main.

Work you haven't sent to a remote exists on one disk only. And origin/main is a memory, not a fact: it was true the last time you checked with the server. An agent reasoning about what is on the server from an out-of-date origin/main can be confidently wrong, so have it fetch first.

Say it to your agent

“Run git fetch origin, then git log --oneline main..origin/main to see what the server has that we don't. Tell me what you find before you do anything about it.”

Push and pull

Also called Fetch

Push sends your commits to the remote. Pull fetches the remote's commits and brings them into your branch. Fetch only downloads, and changes none of your work. Nothing travels until you do one of these.

A push is where a local mistake becomes everyone's, so it deserves more care than anything else an agent runs in a normal day. You can't quietly take back what others have already pulled. Decide in advance whether your agent may push at all.

Say it to your agent

“Push this branch to origin and open a draft pull request. Don't push to main, and don't use any form of force. If the push is refused, stop and tell me what it said.”

Pull request

Also called PR, Merge request, MR

A request, on a hosting site such as GitHub, to merge one branch into another. It shows the diff, collects comments and check results, and keeps the whole discussion attached to the change.

This is the gate between an agent and the real project. Checks run, someone reads it, and the decision goes on the record. It is also where you write what you checked and why you believe it, which makes it proof that a person was in charge.

Say it to your agent

“Open a draft with gh pr create --draft. In the body, write what changed and why, how you tested it with the real command output, and anything you are unsure about. Don't mark it ready, and don't merge it.”

Clone and fork

A clone is a full copy of a repository on your machine, history included. A fork is your own copy on the hosting site, which you can change without asking anyone and later propose changes from.

Cloning is also how someone else's instructions reach your agent: a repository can hold files an agent reads and obeys, such as AGENTS.md or tool settings. Read those before you let it run anything. And an agent with a fork, and no write access to the original, is limited in what it can break.

Say it to your agent

“Clone this repository into a new folder, but before running anything, list what would execute: package.json scripts, any .claude, .cursor or .mcp.json config, and AGENTS.md. Show me those and wait.”

.gitignore

Also called Ignore file

A file listing the names and patterns git should not track, such as dependencies, build output and secrets. Git then behaves as if those files weren't there. It only applies to files git isn't already tracking.

Without one, git add -A commits node_modules and your keys. One line here is a small permanent rule, and better than asking an agent to remember. But it can't undo anything: a secret already committed stays in the history.

Say it to your agent

“Add .env, dist/ and node_modules/ to .gitignore, and run git check-ignore -v .env to prove it matches. If git ls-files shows .env is already tracked, run git rm --cached .env and tell me, because that key needs replacing.”

A history is only a paper trail if someone can read it. The diff says what changed. Nothing else can say why.

Commit message

Also called Conventional Commits

The note attached to a commit. Its first line says what changed, short enough to read in a list, and the body says why. Conventional Commits is a common format for that first line: fix(cart): stop charging VAT twice.

The next session's agent starts knowing nothing and reads git log to catch up, so this is the memory you leave it. It also matters for you: a message giving a real reason shows you understood the change, and "fix stuff" shows nothing. An agent will write vague ones unless told otherwise.

Say it to your agent

“Write the message as type(scope): subject. In the body, say why this change was needed and what you ruled out, not what the diff already shows. Never write "fix bug" or "update code".”

Small commit

Also called Atomic commit

A commit that does one thing and leaves the project working. If you need the word "and" to describe it, it is probably two commits. A rename, a bug fix and a reformat are three, even if they happened in the same hour.

Left alone, an agent makes one huge commit at the end, which can't be reviewed or undone in part, and which hides the moment any bug arrived. Small commits fix all of that. A series also shows the order you thought in, such as failing test, fix, tidy, which is the kind of evidence a single large commit can't give.

Say it to your agent

“Commit in this order, as separate commits: the failing test first, then the smallest fix that makes it pass, then any tidying. Every commit after the first must pass bun test, and the first must say in its message that the test is expected to fail.”

Now the ways back, from the safest to the most dangerous.

Revert

Undoing a commit by adding a new commit that does the opposite. The mistake stays in the history along with its undoing, and nobody else's copy is disturbed.

This is the undo for anything already pushed. It needs no force, it can't lose anyone's work, and it leaves a record of what went wrong and that someone dealt with it. Make it your agent's default, because left to itself it may reach for something worse.

Say it to your agent

“Commit a1b2c3d broke checkout and it is on main. Undo it with git revert a1b2c3d, on a new branch, through a pull request. Don't use reset or force push. Say in the message what broke and how we know.”

Reset

Also called git reset --hard

Moving the current branch to a different commit. Soft leaves your files and staged work alone. Mixed, the default, also clears what is staged. Hard also overwrites your files to match that commit, which destroys uncommitted work.

git reset --hard is the fastest way for an agent to destroy hours of work, because it looks like tidying up. Commits it drops can usually be rescued. Uncommitted changes can't, because git never saved them. Never let an agent run it without asking first.

Say it to your agent

“Don't run git reset --hard, git checkout -- ., git restore . or git clean -f. If you need a clean tree, tell me what would be lost, and use git stash push -u -m "before this task" instead.”

Force push

Also called git push --force, --force-with-lease

Telling the server to replace a branch with yours, even when that throws away commits it has. A normal push can only add. This one can delete other people's work from the server, and it barely mentions it.

This is the git command whose damage lands on other people. An agent reaches for it when a normal push is refused, and a refused push often means someone else's work is in the way. On your own branch after a rebase it is routine. On a shared one, it needs a person.

Say it to your agent

“If a push is refused, stop and tell me why. Don't use --force, -f or a + refspec. If this is your own branch and you rebased it, use git push --force-with-lease --force-if-includes on that branch only, and never on main.”

A few smaller tools, each of which behaves differently when an agent is holding it.

Stash

A shelf for unfinished work. git stash puts your changes to tracked files aside and returns your folder to the last commit, and git stash pop brings them back. New files git isn't tracking yet stay where they are unless you add -u.

A stash has no branch and no reminder, and agents don't remember across sessions, so work stashed "to get a clean tree" can sit there unnoticed for ever. Anything you care about belongs in a commit on a branch, which costs no more and shows up in the history.

Say it to your agent

“If you stash, use git stash push -u -m "why", and pop it before your last step. Run git stash list at the end and show me that it is empty. If it isn't, tell me what is in there.”

Tag

A permanent name for one commit, usually a release such as v1.2.0. Unlike a branch, it doesn't move when you commit. An annotated tag also records who made it, when, and a message.

A tag gives you and your agent a fixed point: compare against v1.2.0, go back to the last release. An agent that moves a published tag breaks that promise for everyone who already has it, so treat tags as write-once.

Say it to your agent

“Tag this release with git tag -a v1.3.0 -m "Adds CSV export", then push it with git push origin v1.3.0. If the tag turns out wrong, make v1.3.1. Never move or delete a tag that is already pushed.”

Blame

Also called Annotate

A view of a file showing, for each line, the last commit that changed it and who made it. The name is unfortunate: you use it to find out why a line exists, not whose fault it is.

Before an agent changes a strange line, it should read why that line is there, since it may be preventing something invisible. That only works if the history is good, and an agent that reformats a file while fixing it makes itself the author of every line.

Say it to your agent

“Before you change line 42 of src/cart.ts, run git blame -w -L 42,42 src/cart.ts, read that commit with git show, and tell me why the line is there. If you still want to change it, say what would break.”

Bisect

A way to find the commit that introduced a bug by halving the range. You name a good commit and a bad one, git checks out the middle, you say good or bad, and it halves again. A thousand commits takes about ten tests.

When an agent is stuck guessing at a bug, bisect stops the guessing, because its answer is one commit. It can run unattended too: give it a script, and it finds the commit while you do something else. It depends on every commit building, and on the commit it names being small enough to read.

Say it to your agent

“v1.2.0 worked and HEAD is broken. Write scripts/repro.sh so it exits 0 without the bug, 1 with it, and 125 if the commit won't build. Then run git bisect start HEAD v1.2.0 and git bisect run scripts/repro.sh, and show me the commit it names.”

Worktree

Also called git worktree

A second working folder attached to the same repository, on its own branch. The folders share one history, but each has its own files, its own staged changes, and its own idea of which branch is current.

Two agents in one folder interfere without any error: one switches branch under the other, and a commit picks up files it shouldn't have. A worktree for each agent stops that, and it is much lighter than a full clone. Files git ignores, such as .env and node_modules, aren't there until you add them.

Say it to your agent

“Make a worktree for this task with git worktree add ../app-search -b search, install dependencies there, and work only in that folder. When it is merged, remove it with git worktree remove ../app-search.”

Git hook

A script git runs by itself at certain moments, such as just before a commit or a push. If the script exits with anything other than 0, git stops. Hooks live in your own copy of the repository and aren't part of its history.

A hook turns "please run the linter", a tool that checks code for known bad habits, into something that happens every time, and its error goes straight to the agent. But it only guards one machine, and a single flag skips it, which an agent under pressure may well use. Treat it as a fast helper, and run the same checks on the server.

Say it to your agent

“Add lefthook with a pre-commit job running gitleaks git --pre-commit --staged --redact, and a pre-push job running bun run typecheck. Show me each one refusing something. Never use --no-verify: if a hook fails, fix the cause or ask me.”

GitHub

Also called Forge, GitLab, Hosting site

A website that hosts git repositories and adds what git lacks: pull requests, reviews, issues, permissions and automated checks on every push. Git is the tool. GitHub is one company's service built around it, and GitLab is another.

Everything on your machine can be skipped by whoever is at the keyboard, and that includes your agent. The server is where rules really hold: branch protection, required checks, required review. An agent with limited access can't talk its way past any of them.

Say it to your agent

“Using gh, add a ruleset for main: require a pull request, require the ci check to pass, and block force pushes and deletions. Then try a direct push to main and show me the refusal.”

A commit you think you lost is rarely gone.

Reflog

Git's private diary of everywhere HEAD has been: each commit, checkout, reset and rebase. When a commit seems lost, it is usually listed here, and you can walk straight back to it.

This is the real undo, and what makes it reasonable to let an agent move fast: anything it committed can usually be recovered, even after a bad reset or rebase. Its limits matter as much. It is local to one machine, it expires, and it knows nothing about work that was never committed.

Say it to your agent

“You reset past my work. Run git reflog, find the last entry before the reset, tell me its hash and message, and make a branch there with git branch rescue and that hash. Don't run gc, and don't reset anything else.”

You now know which commands destroy work. The last step is making sure an agent in a hurry can't run them.

Git guardrails

Rules at three levels that stop dangerous git commands: a deny rule in the agent's settings, a script that inspects each command before it runs, and protection on the server. Each one catches things the others miss.

A line in an instructions file is a request, and an agent trying hard to finish will do what looks like progress. Local rules catch local damage, such as reset and clean, which no server ever sees. The server catches whatever reaches it, however it was spelled. You need both, and each needs testing.

Say it to your agent

“In .claude/settings.json add these to permissions.deny: Bash(git push --force *), Bash(git push -f *), Bash(git reset --hard *), Bash(git clean *). Add a PreToolUse hook that reads the full command and exits 2 on any force push, reset --hard or --no-verify. Then try each command and show me it being refused.”

Do it for real

Reading gets you the words. This gets you the experience behind them, and something to show.

Lose work on purpose, then get it back

You want your first real reset --hard to be one you chose. After this, you will know from experience exactly what comes back and what doesn't, and you won't panic when an agent does it for real.

  1. In a throwaway repository, have your agent make five small commits.
  2. Run git reset --hard HEAD~3, then git log, and see the commits apparently vanish.
  3. Run git reflog, find the entry from before the reset, and run git branch rescue with that hash. Check that the files are back.
  4. Now edit a tracked file without committing, and run git reset --hard again. Search git reflog and git fsck --lost-found for your edit.
  5. Write two sentences on what you could recover and why.

Done when: A saved terminal log showing the rescue working, the uncommitted edit being gone, and your two sentences.

Put guardrails on a real repository

A rule you have never seen refuse anything is only a hope. This is the setup an interviewer can ask you to open and explain, and you will have tried to get around it yourself.

  1. Add deny rules for force push, reset --hard and clean to your agent's settings.
  2. Add a hook that reads the whole command and refuses those, including spellings like git -C . push --force.
  3. On GitHub, add a ruleset for main that blocks force pushes and requires a pull request.
  4. Ask your agent to run each dangerous command, in several spellings, and note which layer stopped which. Use a test branch in a test repository, not real work.
  5. Commit the settings, with a message saying what each rule is there to stop.

Done when: The settings file in a repository, the ruleset page, and a log of each layer refusing a command, with any spelling that got through written down honestly.

Build a history worth reading, then use it

This produces something to point at: a run of commits showing the order you thought in, and proof that it is useful, because you will use it to find a bug. It also shows why small commits pay for themselves.

  1. Pick a small feature in a real project, on its own branch.
  2. Have your agent commit in order: failing test, smallest fix, tidying. Every message needs a type, a subject and a body saying why. Rewrite any it gets vague on.
  3. Open a pull request, and write in it what you checked and how.
  4. On a scratch branch, add a real regression a few commits back, with more commits on top. Write a script that exits 0 or 1 for it, and run git bisect run.
  5. Check that it names the commit you broke. Then make your test flaky on purpose and run it again, and see what it names.

Done when: A pull request whose commit list reads as a story, and a bisect log naming the right commit, plus a note on what happened with the flaky test.

The build, stage 3 of 18

History you can get back

A repo with four commits that say why, a remote to push to, and proof you can lose work and recover it. An empty ratchet log starts here.

About 45 to 75 minutes, on the same app as every other stage. We ran it first and show what went wrong.

Open stage 3 of the build

Gate 3: Version control

0 of 6 right

  1. 1You had three hours of uncommitted edits and two recent commits. The agent says: "I ran git reset --hard HEAD~2 to clean things up." What can you get back?

  2. 2Your agent's bad commit went to main ten minutes ago, and two colleagues have already pulled. How should it be undone?

  3. 3Two agents are working in the same folder on different branches. A commit on one branch turns up containing the other agent's half-finished file, and no error appeared. What is the fix?

  4. 4You find that a live API key was committed and pushed an hour ago. What do you do first?

  5. 5A normal push is refused because the server has commits you don't. The agent says it will fix this with --force-with-lease, since that is the safe kind. What should happen?

  6. 6You added the deny rule Bash(git push --force *) in Claude Code, and it refused when you tried it. Can main still be overwritten?

Get every answer right to install it, and to put a file in your repo. Wrong answers cost nothing.

Next chapterReading and debugging

The workshop

Name a word, or describe what you mean in everyday language.