A standalone blog/ folder (not course content) with drafts for jpaul.me: an announcement, a getting-started piece, then a hybrid weekly series — one post per module for Units 1-2 (posts 03-13) and one per unit for the back half (14-16) plus a capstone finale (17). Each post carries WordPress metadata, a [COURSE LINK] placeholder, and [insert screenshot] blocks for Justin to fill before publishing. README.md holds the manifest + checklist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EghAChc9UbcF78t55mfdf
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]([COURSE LINK]), 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]([COURSE LINK]): 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
experimentintomain. C gains D, E, F. - Kill it: delete
experiment. D, E, F evaporate.mainis 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 ~/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 inlist, and sortlistso 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)"
And now 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 helps)
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.
You can manufacture exactly this 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. Merge one into the other and Git stops cold:
git merge feature/stats
git status # cli.py listed under "Unmerged paths"
And here's where editor-integrated AI earns its keep, because a merge conflict is the sweet spot for it — a small, perfectly bounded reasoning task with both sides and the surrounding code right there. Ask:
"
cli.pyhas a merge conflict on the usage line. I want the final version to list BOTH thestatsandpurgecommands. Resolve the conflict and remove the markers."
It should hand back a single marker-free line. Then you settle it with Git:
git diff # check ONLY what you intended changed; no markers remain
python cli.py # run it — see the merged usage string
git add cli.py
git commit # opens an editor for the merge message; save and close
Once you can read those three lines of markers, conflicts stop being scary and become a five-minute chore. The syntax is identical no matter the file or the project. (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 practice.)
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 -Dand 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 -Dpermanently 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 moremainmoves 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. -Dandgit merge --abortare sharp tools. Force-delete discards unmerged commits with no confirmation;--abortthrows 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 read those <<<<<<< / ======= / >>>>>>> markers, resolved a real conflict to a clean file that runs, and completed the merge.
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.