Files
ai-workflow-course/blog/07-branches-sandboxes.md
T
justin 0f8e7497a4 Module 6: reframe Part C around the AI silently auto-resolving conflicts (#97)
A current frontier editor-agent told to "merge X into Y" resolves the
conflict and completes the merge in one turn, so the learner never sees a
marker. The old Part C assumed Git would stop and ask. Rework the lab into
a three-beat sequence: witness the conflict once (agent stop-on-conflict
idiom, as in Module 26), undo it with `git merge --abort`, then let the AI
merge for real and auto-resolve while the learner does the one job still
theirs: verify with `git diff` after every merge.

Updates the matching surfaces so they tell one story: learning objective
#4, the Merge-conflicts key concept, the AI-angle bullet, the
Where-it-breaks bullet, Check-for-understanding, the blog mirror, and the
make-conflict.sh on-screen guidance (read the markers yourself first).

Closes #97

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCv6VTpBG6Zo4xR4AvUQpj
2026-06-23 09:03:40 -04:00

16 KiB

Let the AI Try Something Reckless: On a Branch

There's a specific flavor of hesitation I want to talk you out of.

You've got an idea (rewrite the storage layer, try a completely different CLI structure, add a feature that touches four files) and you suspect the AI could just do it. But you're not sure it'll work, you're not sure you'll like it, and the thing it'd be operating on is your actual, working code. So you don't ask. Or you ask, get a sprawling multi-file change back, and now you're squinting at it going "...how do I undo all of this if it's wrong?"

That hesitation is the tax you pay for not having a sandbox. This post is about removing it.

If you're new here: this is part of The Workflow, a free course about all the engineering scaffolding around AI-generated code (the version control, the editor integration, the review reflex) that the model itself doesn't give you. A couple of posts back we installed the safety net: Git, framed as undo for the AI. That safety net was perfect for one bad edit: commit, then git restore if the AI makes a mess. Today we go one size up: isolating a whole line of experimental work so you can keep it or throw it away as a single unit. That's a branch.

What a branch actually is (it's less than you think)

Strip the mystique and a branch is a named, movable pointer to a commit. That's the entire definition.

Your commit history is a chain of snapshots; you built that intuition with git commit. A branch is just a sticky label that points at one of those snapshots and slides forward every time you commit. When you ran git init -b main to start your repo, Git made one branch for you and named it main. Every commit since moved the main label forward. You've been "on a branch" this whole time without thinking about it.

Here's the part that surprises people with an ops background, because it cut against my instincts too: creating a branch copies nothing. No second folder. No duplicated files. No disk cost worth mentioning. Git writes a new label pointing at the same commit you're standing on, and that's it. Which is exactly why branches are cheap enough to be disposable, and disposable is the whole property we're after.

git branch                       # list branches; the * marks the one you're on
git switch -c experiment         # create a branch called "experiment" and switch to it
git switch main                  # switch back to main
git branch -d experiment         # delete a branch you've already merged
git branch -D experiment         # FORCE-delete a branch, merged or not (the "throw it away" button)

(Quick aside for anyone who's googled this before: you'll see git checkout -b experiment all over the internet. It does the same thing as git switch -c experiment. The newer switch/restore commands got split out of the old overloaded checkout so they actually say what they mean. Use whichever you like; I'll use switch.)

The reframe: a branch is a scratch VM you can blow away

You already have the mental model for this, you just file it under different names. A branch is the Git equivalent of a scratch VM you snapshot and roll back, a staging box nobody depends on, a feature flag you can rip out. You spin one up precisely because you're about to do something you might regret, and you want a clean way to make it never have happened.

Picture it as two tracks:

main:         A───B───C                  (always runnable; your "known good")
                       \
experiment:             D───E───F         (the AI's bold attempt, however messy)

While you're on experiment, main is frozen at C: runnable, shippable, untouched. The AI can leave experiment as a smoking crater at F and main genuinely does not care. When you're done, you make exactly one decision:

  • Keep it: merge experiment into main. C gains D, E, F.
  • Kill it: delete experiment. D, E, F evaporate. main is still exactly C, as if nothing happened.

That second path (kill it, no trace) is the one this whole concept exists for. It's the difference between "I now have to carefully undo everything the AI did" and "I delete the branch."

One more thing that feels like magic the first time: when you git switch to another branch, Git rewrites the files in your folder to match it. Switch to experiment and the AI's half-built feature appears in your editor. Switch back to main and it vanishes. Same folder, different contents, instantly. (This is also why Git won't let you switch with uncommitted changes that'd get clobbered; switching would silently throw work away. The fix is the habit you already have: commit before you switch.)

[insert a screenshot referencing git log --oneline --graph showing main and an experiment branch diverging here]

The lab: let the AI go bold on tasks-app

Enough theory. The course runs on a tiny example app called tasks-app (a little command-line to-do tracker), and this is where branches stop being abstract. Make sure you're on a clean main first (git status should say "nothing to commit"), then spin up an experiment:

cd ~/ai-workflow-course/tasks-app
git switch main
git status                       # must be clean
git switch -c experiment/priorities
git branch                       # the * is now on experiment/priorities

Now give your editor-integrated AI a deliberately bold task, the kind you'd hesitate to run straight on main:

"Add task priorities (low/medium/high) to this app. Store a priority on each task, let me set it when adding (add "thing" --priority high), show it in list, and sort list so high priority comes first. Change whatever files you need to."

Let it edit tasks.py and cli.py freely. This is a multi-file change: exactly the kind that's nerve-wracking on main and completely relaxed on a branch. Review what it did, then commit on the branch:

git diff                         # read what it actually changed
python cli.py add "ship module 6" --priority high
python cli.py add "water plants" --priority low
python cli.py list               # see if priorities work and sort
git add .
git commit -m "Add task priorities (experiment)"

The payoff: prove the isolation. Switch back to main and watch the whole feature disappear:

git switch main
python cli.py list               # no priorities: main is exactly as you left it

Sit with that for a second. Your bold change exists only on the branch. main never saw it. That's the entire point of the module in two commands.

Decide its fate: keep it or kill it

Keep it (merge): switch to the branch you want to receive the work, then merge:

git switch main
git merge experiment/priorities      # likely a fast-forward: main slides up to the branch
git log --oneline --graph            # straight line = fast-forward
python cli.py list                   # the feature is now on main
git branch -d experiment/priorities  # branch did its job; -d is the safe delete

Worth knowing there are two flavors of merge, and Git picks for you. If main hasn't moved since you branched, you get a fast-forward: Git just slides the main label up to F, history stays a straight line. If main did move on (you committed to it while the experiment was off doing its thing), the two lines diverged and Git stitches them with a merge commit that has two parents. You don't choose; you just recognize them in the graph (straight line vs. a visible fork-and-join).

Kill it (discard): this is the one I really want you to feel. The AI tried something, you looked, you don't want it. You don't undo anything. You don't restore file by file. You switch away and delete:

git switch main                      # files snap back to known-good main
git branch -D experiment/priorities  # force-delete the unmerged branch
git log --oneline                    # no trace of the experiment on main

That's it. Notice what you did not do: no file-by-file restore, no manual undo, no hunting through diffs. You deleted a label and the entire experiment was gone. The whole bold attempt cost you one branch and one delete.

This is the mental shift the module is selling. When discarding is this cheap, you stop being precious about what you let the AI try. Risky refactor? Branch it. Want to compare two approaches? A branch each; keep the winner, delete the loser. The branch becomes your unit of "maybe."

Merge conflicts: when two changes collide (and the AI resolves them before you see them)

Most merges just work; Git is genuinely good at combining changes that touch different lines. A conflict only happens when two branches changed the same lines in different ways, and Git refuses to guess which you meant. It stops and marks the collision right inside the file:

<<<<<<< HEAD
    print("usage: python cli.py [add <title> | list | done <index> | purge]")
=======
    print("usage: python cli.py [add <title> | list | done <index> | stats]")
>>>>>>> feature/stats

Read it like this. Everything from <<<<<<< HEAD to ======= is your current branch's version. Everything from ======= to >>>>>>> feature/stats is the incoming version. The markers are real text Git inserted into your file. Resolving means editing the file so it holds the version you want (often a blend of both, here a usage string listing both commands) and deleting all three marker lines.

Here's the twist, and it's the reason I'm not going to hand you a "read the markers, edit them out" drill and call it a skill. You can manufacture exactly this collision in tasks-app: make one branch where the AI adds a stats command (updating the usage string), then a separate branch off main where it adds a purge command (also updating the usage string). Both edit the same line. Then tell a current editor-agent to "merge feature/stats into feature/purge," and watch what doesn't happen: it doesn't stop. It reads both sides, picks the resolution, finishes the merge, and reports a clean result, all in one turn. You never see a marker. From your chair the conflict simply didn't occur.

That's the sweet spot for the AI (a small, perfectly bounded reasoning task with both sides and the surrounding code right there) and it's also the trap. So do this once, deliberately, to see the machine: ask it to stop instead of resolving.

"Merge feature/stats into feature/purge. If it conflicts, stop and show me the conflict; don't resolve it yet."

Now Git pauses on the unmerged file and you can read the markers above with your own eyes. Then git merge --abort to rewind, and let the agent do it for real with no guard rail, the way you actually would:

"Merge feature/stats into feature/purge; the usage line collides, and the final version should list BOTH commands."

It resolves silently and the merge lands. And here is the only part that's still your job, conflict or no conflict:

git diff HEAD~1                  # what the merge actually changed; confirm no markers, both commands present
python cli.py                    # run it: see the merged usage string
python cli.py stats && python cli.py purge   # both actually work

That git diff after every merge is the whole skill now. Not "edit the markers by hand," which the AI did for you before you could blink, but "know a conflict can happen and check the silent resolution," because a resolution that runs cleanly can still be wrong and it won't leave an error behind to warn you. (And if your AI's edits didn't happen to collide (they're nondeterministic), the course ships a little make-conflict.sh helper that manufactures one deterministically so you can still see the markers at least once.)

The AI angle: why this matters more now

Everything above is standard Git that predates the current AI wave by a decade. So why am I telling IT pros who already know Git to care? Because AI changes the cost-benefit:

  • The branch is the blast-radius container for an autonomous attempt. An agent editing your files directly is fast and confident, including when it's confidently wrong across four files. On main, cleaning that up is a chore. On a branch, you delete the branch. The riskier and more hands-off the AI work, the more a branch earns its keep.
  • "Throw it away" is the feature, not the failure. With copy-paste, a rejected AI attempt still cost you the manual paste-in and the manual rip-out. With a branch it costs nothing: git branch -D and it never happened. That flips the economics: you can let the AI try things you'd never risk if undoing were expensive.
  • Compare, don't commit-and-hope. Ask for approach A on one branch and approach B on another. Run both. Keep the winner. Cheap A/B experiments on implementation: painful without branches, trivial with them.

Where this breaks (because I'd rather you trust me)

The honest limits, so you don't over-trust the sandbox:

  • A branch isolates files in the repo, nothing else. Switching branches rewrites your tracked files; it does not roll back a database your app wrote to, files Git is ignoring, running processes, or anything outside version control. If the AI's experiment ran a migration or wrote to tasks.json (which is git-ignored), deleting the branch won't undo that. The sandbox is the repo, not the world.
  • Branches are local until you push them. Everything here lives on your laptop. A branch isn't shared, backed up, or visible to anyone until there's a remote (that's a later post). Right now git branch -D permanently deletes work that exists nowhere else. Treat an unpushed branch as exactly as fragile as the rest of your local-only repo.
  • The AI can resolve a conflict into something plausible and wrong. It sees both sides and the intent, which makes it good at this, but "good" isn't "trusted." A resolution that runs cleanly can still mean the wrong thing: silently keeping the worse of two changes, or blending two behaviors into one that satisfies neither. The git diff + run-it check isn't ceremony; it's the actual safeguard.
  • Long-lived branches drift and conflict harder. The longer a branch lives away from main, the more main moves underneath it and the gnarlier the eventual merge. The defense is the same as "commit often": branch small, merge soon, delete promptly. A branch that's been open three weeks is a future conflict, not a sandbox.
  • -D and git merge --abort are sharp tools. Force-delete discards unmerged commits with no confirmation; --abort throws away an in-progress resolution. Both are exactly what you want at the right moment and a foot-gun at the wrong one. Know which one you're reaching for.

You're done when

You've created a branch, let the AI make a multi-file change on it, and confirmed main was untouched by switching back and watching the change vanish. You've discarded an experiment with git branch -D and seen main show no trace, and you've merged one in and seen it land. You can explain in one sentence why a branch costs essentially nothing (it's a movable pointer, not a copy). And you've seen those <<<<<<< / ======= / >>>>>>> markers at least once, then watched the AI merge for real and resolve the conflict silently, and you verified the result with git diff even though no marker was ever shown to you.

When "let the agent try something wild" feels like a one-line decision instead of a risk assessment, you've got it.

Next up: branches let you run one experiment at a time, because switching swaps your whole folder. The moment you want two agents working in parallel without stepping on each other, you've hit the edge of branches, and that's exactly what worktrees solve. That's the next post.

Tried this on a real experiment: kept one, threw one away? Tell me how it went in the comments. I read them, and the rough edges you hit are what make the course better.