It worked last week. Today the same check fails, and git log is a wall of messages that all look innocent. Scanning every commit by hand is slow. git bisect binary-searches history: you mark a known-good snapshot and a known-bad one, then Git jumps to the midpoint until it names the first bad commit.

Day-one snapshots live in Git Without the Panic. Branches live in Stay Off Main. Undo lives in When Push Fails. This page is the hunt: a check you can run locally, and the commit that first made it fail.

Warm-up: a linear history that breaks

A local chain is enough — no remote required. From a scratch folder, init on main, commit a healthy status file, then add a few harmless commits so the later bug is not sitting next to the start of history:

mkdir -p git-bisect-lab
cd git-bisect-lab
git init
git branch -M main

echo 'ok' > status.txt
git add status.txt
git commit -m "Add status"

for n in 1 2 3; do
  echo "note $n" >> notes.txt
  git add notes.txt
  git commit -m "Notes $n"
done

echo 'App v1' > README.md
git add README.md
git commit -m "Add README"

Now break the check, then add two unrelated commits so HEAD is bad but the culprit is not the tip:

echo 'broken' > status.txt
git add status.txt
git commit -m "Tweak status output"

echo 'more notes' >> notes.txt
git add notes.txt
git commit -m "More notes"

echo 'v1' > CHANGELOG.md
git add CHANGELOG.md
git commit -m "Add changelog"

Eight commits, newest first. Copy the Add status SHA — that is your last known good. HEAD (Add changelog) is bad:

git log --oneline
<sha> Add changelog
<sha> More notes
<sha> Tweak status output
<sha> Add README
<sha> Notes 3
<sha> Notes 2
<sha> Notes 1
<sha> Add status

Everything below runs inside git-bisect-lab unless noted. Delete the folder when you finish.

Note: Prefer git switch for changing branches in everyday work. During bisect you do not switch yourself — Git moves HEAD to each candidate commit. That is a detached HEAD. Leave it until git bisect reset.

Mental model: binary search over commits

Bisect treats history as a sorted list. You answer one question at each step: is this snapshot good or bad? Git halves the remaining range until a single commit is the first that fails.

“Good” means your check passes. “Bad” means it fails. The check can be as small as reading a file — it only has to be the same question at every commit. On main (HEAD, bad) it fails:

grep -q '^ok$' status.txt; echo $?
1

Note: Stay on main until git bisect start. After that, Git checks out other commits for you. Do not git switch main to “fix” detached HEAD mid-session.

Hero use case: walk to the first bad commit

You know HEAD is broken and Add status was fine. Let Git pick the midpoints.

Start and mark the ends

Start a session, mark the current commit bad, then mark the old SHA good:

git bisect start
git bisect bad
git bisect good <add-status-sha>

Git checks out a commit near the middle and prints how many revisions are left. git status shows you are bisecting, with HEAD detached:

git status
HEAD detached at <sha>
You are currently bisecting, started from branch 'main'.

Test, mark, repeat

Run the same check. Exit 0 means this snapshot is good; 1 means bad:

grep -q '^ok$' status.txt; echo $?

If it passes, mark good. If it fails, mark bad. Each mark checks out the next midpoint:

git bisect good
# or, if the check failed:
git bisect bad

Repeat until Git prints the culprit instead of “Bisecting:”:

<sha> is the first bad commit
commit <sha>
Author: ...
    Tweak status output

That SHA is the first commit where the check fails — here, Tweak status output. Later commits are also bad, but they did not introduce the break.

Note: You are still in the session after that message. HEAD is still detached. Reset before you start any other work.

Leave detached HEAD with bisect reset

End the session and return to the branch you started on (main in this lab):

git bisect reset
git status

You should be on main again with an attached HEAD. Reset ends the search; it does not delete the first-bad commit.

Always git bisect reset when you are done — after a find, a wrong mark, or a finished bisect run. git switch is the everyday branch move; it is not how you exit bisect.

Automate the walk with bisect run

The manual loop is the same test every time. Hand it to Git: exit 0 for good, exit 1 for bad. Recreate the range, then run grep as the check:

git bisect start
git bisect bad
git bisect good <add-status-sha>
git bisect run grep -q '^ok$' status.txt

Git checks out each midpoint, runs the command, and marks good or bad from the exit code. It prints the same first-bad commit. You are still bisecting — reset:

git bisect reset

Note: git bisect run treats exit 0 as good and 1–127 (except 125) as bad. Exit 125 means skip that commit (same idea as git bisect skip below).

Skip, dirty trees, and merges

If Git lands on a commit you cannot test, skip it instead of guessing good or bad:

git bisect skip

Git picks another commit in the range. Too many skips can leave a range instead of a single first-bad SHA.

Start from a clean working tree. Uncommitted edits that would be overwritten make the next checkout fail. Commit them on a branch, or discard disposable edits with git restore, then start bisect.

This lab is linear on purpose. On a history full of merge commits, the first bad commit might be a merge. Inspect that commit’s parents if you land there; the bug often came in on one side. Keep feature work off main as in Stay Off Main so the hunt stays a straight line when you need it.

Quick reference card

GoalCommand
Start a sessiongit bisect start
Mark current (or HEAD) badgit bisect bad
Mark an old commit goodgit bisect good <sha>
Mark this checkout good / badgit bisect good / git bisect bad
Skip a commit you cannot testgit bisect skip
Automate the walkgit bisect run <cmd>
Leave detached HEADgit bisect reset
See the range while huntinggit status

Practice drills

Inside git-bisect-lab (recreate the eight-commit chain if needed):

  1. From main, start bisect, mark HEAD bad and Add status good, then walk with grep -q '^ok$' status.txt until Git prints Tweak status output as the first bad commit.
  2. After that result, run git bisect reset and confirm git status shows you on main (not detached).
  3. Repeat the same range with git bisect run grep -q '^ok$' status.txt and confirm it names the same first-bad commit, then reset.
  4. Start a new session, git bisect skip once when Git checks out a midpoint, finish the hunt, and reset.
  5. Make an uncommitted edit to status.txt, try to mark good/bad so Git must check out another commit, see it refuse a dirty tree, restore the file, then bisect successfully.

Solid answers — clear and portable:

git switch main
git bisect start
git bisect bad
git bisect good <add-status-sha>
grep -q '^ok$' status.txt; echo $?
git bisect good   # or git bisect bad, matching the exit code
# repeat until Git prints: Tweak status output is the first bad commit

git bisect reset
git status

git bisect start
git bisect bad
git bisect good <add-status-sha>
git bisect run grep -q '^ok$' status.txt
git bisect reset

git bisect start
git bisect bad
git bisect good <add-status-sha>
git bisect skip
# then good/bad (or run) until it finishes
git bisect reset

echo 'dirty' >> status.txt
git bisect start
git bisect bad
git bisect good <add-status-sha>   # checkout often fails here
git restore status.txt
git bisect reset
git bisect start
git bisect bad
git bisect good <add-status-sha>
git bisect run grep -q '^ok$' status.txt
git bisect reset

If those five feel routine, you already practice the hunt: mark a known-good commit and a known-bad one, let Git binary-search, then always reset. Snapshot basics remain in Git Without the Panic. When you already know the commit you want to copy onto another branch, that is Cherry-Pick — a later skill. Next, recover a commit after a reset that went too far with git reflog.

Next optional step Recover a commit after a reset that went too far. Reset Too Far: Recover Commits With git reflog