How to buildOctober 12, 202611 min read
Vibe coding: from prototype to productStep 2 of 7

Git in AI-assisted development: small steps, branches and rolling back an agent

In short

Treat the agent like a new teammate: every task gets its own branch, you read every change in git diff before committing, and you stage only the hunks you want with git add -p. Roll back a bad step according to where it is: uncommitted work with git restore, a pushed commit with git revert, a local unpushed commit with git reset, keeping git reflog as a safety net. Hand a parallel task to a second agent session in its own git worktree.

TutorialLevel: BeginnerVibe CodingPractice: ~1.5–3 h

After this you will be able to

  • run an agent's work on its own branch and size up its changes with git status --short and git diff --stat before committing
  • stage only the hunks you want with git add -p and drop the rest with git restore and git clean -n / -f
  • pick the right rollback for where the mistake is — restore, revert or reset — and recover a dropped commit from git reflog
  • run a second agent task in a separate git worktree and find the commit that broke a test with git bisect run

Verified with

Git 2.54 (Alpine Linux 3.24, Docker)Git 2.55 (Git for Windows, Git Bash)Node.js 24 LTS (24.20)

Checked against docs: October 12, 2026

Contents
  1. 01Why an agent needs Git when the chat has an undo
  2. 02Setup: .gitignore for secrets and a branch per task
  3. 03Read the diff before you commit
  4. 04Commit only what you need: add -p, restore, clean
  5. 05Rolling back a bad step: revert, reset and reflog
  6. 06A second task in parallel: git worktree
  7. 07Practice: repeat, modify, find the culprit commit

Why an agent needs Git when the chat has an undo

In a single turn an agent edits several files, runs commands, creates scratch files and sometimes commits on its own. That is exactly why it is useful: it gets through the routine fast. But once the agent's edits are mixed with yours and with a previous attempt, it gets hard to tell what broke and what is worth keeping. Git turns the agent's work into a chain of small steps, each of which you can read, check and undo on its own.

A tool's built-in undo does not replace that. Claude Code has checkpoints: /rewind restores files the agent changed with its file-editing tools. The documentation lists the limits plainly: changes made by shell commands such as rm, mv and cp are not tracked, background subagents' edits are not restored, snapshots are deleted by default about 30 days after the session last saved one, and checkpoints are “not a replacement for version control”. A commit, on the other hand, outlives the session, is visible to your team and ends up in a pull request.

BranchAgent worksDiffSelect and testCommit
One agent step is one small commit you have read

The running example is a small pricing module for an online shop in JavaScript, tested with the built-in node:test. To make the output repeatable, the “agent's” edits were replayed by a script and commit dates were pinned. Every Git command and its output is real, taken from a run in a container with Git 2.54; the same scenario was repeated in Git for Windows 2.55 and the commit hashes matched. The basics — commits, branches, merging — are covered in the “Git from scratch” lesson; here we only cover what changes when an agent writes the code.

Setup: .gitignore for secrets and a branch per task

Before the first task, check two things: secrets cannot end up in a commit, and the agent is not working on the main branch. Create the branch before the agent starts, with a clean working tree — then everything that shows up in the diff was done by the agent.

terminal
$ git init -q
$ printf 'node_modules/\n.env\n.env.*\n!.env.example\n.claude/worktrees/\n' > .gitignore
$ printf 'PAYMENT_API_KEY=YOUR_API_KEY\n' > .env
$ printf 'PAYMENT_API_KEY=\n' > .env.example
$ git add .
$ git status --short
A  .env.example
A  .gitignore
A  package.json
A  src/price.js
A  test/price.test.js
$ git check-ignore -v .env .env.example
.gitignore:2:.env	.env
$ git commit -q -m "Pricing module with tests"
$ git switch -c agent/discount-cap
Switched to a new branch 'agent/discount-cap'
Git 2.54, Node.js 24.20, node:24-alpine container (real output). .env.example is missing from the check-ignore output: the !.env.example rule brings the template back into the repository, while .env itself never reached the index.

.env, .env.*

Why it matters with an agent
Local keys. Agents often create .env.local or .env.test themselves — the .env.* pattern covers those too

!.env.example

Why it matters with an agent
The template without values stays in the repository so the agent and your team know the variable names

node_modules/

Why it matters with an agent
Dependencies are reinstalled with npm install; they do not belong in history

.claude/worktrees/

Why it matters with an agent
Where Claude Code creates working directories when started with --worktree; its documentation recommends ignoring this path

An agent/ prefix in the branch name shows the agent drove it and makes clean-up easy: git branch --list 'agent/*' lists every such branch. One task, one branch: if the agent needs to do two unrelated things, that is two branches and two pull requests.

Read the diff before you commit

The task for the agent: “cap the discount at 50%”. The agent reports it is done. The report describes intent; the diff shows the result — so the review starts in Git, not in the reply text.

terminal
$ git status --short
 M src/price.js
 M test/price.test.js
?? agent-notes.tmp
?? test/discount-cap.test.js
$ git diff --stat
 src/price.js       | 8 ++++++--
 test/price.test.js | 2 +-
 2 files changed, 7 insertions(+), 3 deletions(-)
$ git diff src/price.js
diff --git a/src/price.js b/src/price.js
index 3f1a6d4..5231abd 100644
--- a/src/price.js
+++ b/src/price.js
@@ -1,7 +1,10 @@
-// Final price after a percentage discount
+const MAX_DISCOUNT = 50;
+
+// Final price after a percentage discount (capped at MAX_DISCOUNT)
 export function finalPrice(price, discountPercent) {
   if (price < 0) throw new RangeError("price must be >= 0");
-  return price - (price * discountPercent) / 100;
+  const pct = Math.min(discountPercent, MAX_DISCOUNT);
+  return price - (price * pct) / 100;
 }
 
 // Sum of line items: [{ price, qty }]
@@ -11,5 +14,6 @@ export function cartTotal(items) {
 
 // Formats an amount: 9000 -> "9000 KZT"
 export function formatPrice(amount) {
+  console.log("formatPrice", amount);
   return `${amount} KZT`;
 }
Git 2.54, Node.js 24.20, node:24-alpine container (real output).

git status --short is the map: M is a modified file, ?? is a new file Git does not know yet. git diff --stat shows the scale: if a one-function task touched twenty files, stop and find out why. One detail matters: git diff does not show new untracked files, so test/discount-cap.test.js and agent-notes.tmp only appear in status. To get a new file into the regular diff, mark it with git add -N — the documentation describes it as recording only the intent to add the path.

The diff of src/price.js has two hunks (a hunk is a block of changes with a few lines of context around it). The first implements the task. The second adds a debug console.log to formatPrice, which nobody asked to touch. One line also changed in test/price.test.js — we will look at it in the next section.

  • Changes outside the task: files reformatted “while at it”, renames, config edits
  • Debug output and temporary flags: console.log, print, debug: true
  • Disabled or weakened tests: test.skip, .only, changed expected values
  • New dependencies in package.json and the lock file
  • Untracked files: the agent's notes, dumps, copies of files
  • Keys, tokens and personal data in code, tests and the commit message

This is a check of scope and hygiene. Logic, access control and error handling are reviewed separately, with the checklist from the lesson on reviewing AI-written code.

Commit only what you need: add -p, restore, clean

git add -p shows the hunks one by one and asks whether to add each to the index (the staging area — what goes into the next commit). In a terminal you answer with keys; in the verification run the y and n answers were piped in with printf, which is why there is no line break after the prompts.

terminal
$ printf 'y\nn\n' | git add -p src/price.js
diff --git a/src/price.js b/src/price.js
index 3f1a6d4..5231abd 100644
--- a/src/price.js
+++ b/src/price.js
@@ -1,7 +1,10 @@
-// Final price after a percentage discount
+const MAX_DISCOUNT = 50;
+
+// Final price after a percentage discount (capped at MAX_DISCOUNT)
 export function finalPrice(price, discountPercent) {
   if (price < 0) throw new RangeError("price must be >= 0");
-  return price - (price * discountPercent) / 100;
+  const pct = Math.min(discountPercent, MAX_DISCOUNT);
+  return price - (price * pct) / 100;
 }
 
 // Sum of line items: [{ price, qty }]
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]? @@ -11,5 +14,6 @@ export function cartTotal(items) {
 
 // Formats an amount: 9000 -> "9000 KZT"
 export function formatPrice(amount) {
+  console.log("formatPrice", amount);
   return `${amount} KZT`;
 }
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]?
Git 2.54, Node.js 24.20, node:24-alpine container (real output). The first hunk is staged (y), the console.log hunk is skipped (n).

y / n

What it does
Stage the hunk / skip it

s

What it does
Split the hunk when there are unchanged lines between the changes

e

What it does
Edit the hunk by hand

a / d

What it does
Stage / skip this and all later hunks in the file

q

What it does
Quit without staging the remaining hunks

?

What it does
Help for every key

Now the test. The agent marked the negative-price check as test.skip: it failed against the agent's first version, so the agent switched it off. A disabled test is a common side effect of “get the tests green at any cost”, and that change must not be committed. We restore the file, stage the agent's useful new test and clean up.

terminal
$ git diff test/price.test.js
diff --git a/test/price.test.js b/test/price.test.js
index dcef809..ae7c7ac 100644
--- a/test/price.test.js
+++ b/test/price.test.js
@@ -6,7 +6,7 @@ test("applies a percentage discount", () => {
   assert.equal(finalPrice(10000, 10), 9000);
 });
 
-test("rejects a negative price", () => {
+test.skip("rejects a negative price", () => {
   assert.throws(() => finalPrice(-1, 10), RangeError);
 });
 
$ git restore test/price.test.js
$ git add test/discount-cap.test.js
$ git clean -n
Would remove agent-notes.tmp
$ git clean -f
Removing agent-notes.tmp
$ git restore src/price.js
$ git status --short
M  src/price.js
A  test/discount-cap.test.js
$ node --test 2>&1 | grep -E '^(✔|✖|ℹ (pass|fail|skipped))'
✔ caps the discount at 50% (3.076503ms)
✔ applies a percentage discount (1.828563ms)
✔ rejects a negative price (0.810228ms)
✔ keeps fractional amounts exact (0.641402ms)
ℹ pass 4
ℹ fail 0
ℹ skipped 0
$ git commit -m "Cap discount at 50%"
[agent/discount-cap a2720be] Cap discount at 50%
 2 files changed, 12 insertions(+), 2 deletions(-)
 create mode 100644 test/discount-cap.test.js
Git 2.54, Node.js 24.20, node:24-alpine container (real output). After the clean-up the working tree matches the index, so the tests check exactly what will be committed.
  1. git restore test/price.test.js

    Restores the file from the index. Changes discarded this way are not saved anywhere — run it after reading the diff, not instead of it.

  2. git add test/discount-cap.test.js

    The agent's new test checks the task, so stage it before cleaning up; otherwise git clean deletes it too, since to Git it is just another untracked file.

  3. git clean -n, then git clean -f

    Dry run first: -n only shows what would be removed. Without -f the command removes nothing by default — that is the clean.requireForce setting.

  4. git restore src/price.js

    Drops the skipped console.log hunk from the working tree. The staged hunk is already in the index and is not lost.

  5. node --test and git commit

    The tests pass on the same code that is being committed — including the negative-price check we brought back.

Rolling back a bad step: revert, reset and reflog

Agents can commit on their own. The agent's next step rounded the final price and committed straight away — without running the tests.

terminal
$ git log --oneline -3
d8f1269 Round final price to whole tenge
a2720be Cap discount at 50%
ee5d239 Pricing module with tests
$ node --test 2>&1 | grep -E '^(✔|✖|ℹ (pass|fail))'
✔ caps the discount at 50% (0.950324ms)
✔ applies a percentage discount (1.136211ms)
✔ rejects a negative price (0.618073ms)
✖ keeps fractional amounts exact (1.031319ms)
ℹ pass 3
ℹ fail 1
✖ failing tests:
✖ keeps fractional amounts exact (1.031319ms)
$ git show --stat --oneline HEAD
d8f1269 Round final price to whole tenge
 src/price.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git revert --no-edit HEAD
[agent/discount-cap 7ebde6d] Revert "Round final price to whole tenge"
 Date: Mon Oct 12 10:00:00 2026 +0500
 1 file changed, 1 insertion(+), 1 deletion(-)
$ node --test 2>&1 | grep -E '^ℹ (pass|fail)'
ℹ pass 4
ℹ fail 0
Git 2.54, Node.js 24.20, node:24-alpine container (real output). grep keeps only the result lines of the node --test report.

If the branch has been pushed or has an open pull request, do not rewrite history. git revert records a new commit that reverses the faulty one: history shows both the mistake and its undo, and nothing breaks for your teammates. According to the documentation, revert needs a clean working tree with no uncommitted changes.

A different case: the agent set out to rewrite the module, committed the attempt locally, and you decided the direction is wrong. The commit has not been pushed anywhere, so it can be removed from the branch with git reset --hard.

terminal
$ git log --oneline -2
2aaafa5 WIP: rewrite pricing as a class
7ebde6d Revert "Round final price to whole tenge"
$ git status --short
$ git reset --hard HEAD~1
HEAD is now at 7ebde6d Revert "Round final price to whole tenge"
$ git log --oneline -1
7ebde6d Revert "Round final price to whole tenge"
$ git reflog -3
7ebde6d HEAD@{0}: reset: moving to HEAD~1
2aaafa5 HEAD@{1}: commit: WIP: rewrite pricing as a class
7ebde6d HEAD@{2}: revert: Revert "Round final price to whole tenge"
$ git branch rescue/pricing-class HEAD@{1}
$ git log --oneline -1 rescue/pricing-class
2aaafa5 WIP: rewrite pricing as a class
Git 2.54, Node.js 24.20, node:24-alpine container (real output). git status --short is empty before the reset: there are no uncommitted changes for --hard to wipe.

git reflog records where HEAD pointed after every commit, reset and branch switch. HEAD@{1} means “where HEAD was one move ago”: we used it to create the rescue/pricing-class branch and keep the dropped attempt in case something in it turns out useful. By default, reflog entries for commits no longer reachable from a branch are kept for 30 days, the rest for 90.

The agent broke a file, nothing committed yet

Command
git restore file or git restore -p
What you lose
The discarded changes — they cannot be recovered

Something extra got staged

Command
git restore --staged file
What you lose
Nothing: the changes stay in the working tree

A faulty commit is already pushed or in a pull request

Command
git revert <commit>
What you lose
Nothing: history is not rewritten

A local, unpushed commit is not needed

Command
git reset --hard HEAD~1
What you lose
Uncommitted changes; the commit itself can be recovered from reflog

Drop the commit but keep its changes to rework

Command
git reset --soft HEAD~1
What you lose
Nothing: the changes stay staged

A second task in parallel: git worktree

While one agent session finishes the discount, the second task — the price format on the receipt — can go to another session. If both work in the same folder, their edits get mixed. git worktree creates a second working directory on its own branch: the repository and history are shared, but each directory has its own files, index and HEAD.

terminal
$ git worktree add ../shop-receipt -b agent/receipt-format
Preparing worktree (new branch 'agent/receipt-format')
HEAD is now at 7ebde6d Revert "Round final price to whole tenge"
$ git worktree list
/lab/shop         7ebde6d [agent/discount-cap]
/lab/shop-receipt 7ebde6d [agent/receipt-format]
Git 2.54, Node.js 24.20, node:24-alpine container (real output).

In the second directory the agent changes formatPrice. Git will not remove a working directory with uncommitted work: according to the documentation, git worktree remove only removes clean worktrees, others only with --force.

terminal
$ git -C ../shop-receipt status --short
 M src/price.js
$ git worktree remove ../shop-receipt
fatal: '../shop-receipt' contains modified or untracked files, use --force to delete it
$ git -C ../shop-receipt commit -q -am "Show prices with two decimals"
$ git worktree remove ../shop-receipt
$ git worktree list
/lab/shop 7ebde6d [agent/discount-cap]
$ git branch --list 'agent/*'
* agent/discount-cap
  agent/receipt-format
$ git log --oneline -1 agent/receipt-format
f12f10d Show prices with two decimals
Git 2.54, Node.js 24.20, node:24-alpine container (real output).

The directory is gone, but the agent/receipt-format branch and its commit remain: you can review it, merge it or open a pull request from it. Claude Code does the same with a single flag.

  • claude --worktree name (or -w) creates a working directory in .claude/worktrees/<name>/ on a new branch worktree-<name>; without a name, Claude Code generates one.
  • A worktree is a fresh checkout: dependencies have to be installed again, and ignored files such as .env are not copied. A .worktreeinclude file in .gitignore syntax copies them.
  • The same branch cannot be checked out in two worktrees at once: git worktree add refuses.
  • When you exit the session, Claude Code checks the directory: a clean unnamed worktree is removed together with its branch; one with changes or commits triggers a prompt to keep or remove it.

Practice: repeat, modify, find the culprit commit

Do the exercises on your own project or on the lesson's pricing module: its sources, the “agent” edits and the verification script are kept in the archive of verification projects.

How this was verified4 · passedchecked 12 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.