Bug Hunting with git bisect
git bisect is a binary search over your commit history. You tell Git which commit is good (no bug) and which is bad (bug present). Git checks out the commit in the middle, you test, and you mark it good or bad. Git keeps halving the range until it pinpoints the exact commit that introduced the bug. For 1024 commits, that's about 10 tests — log2(N) instead of linear.
When to reach for bisect
A test that used to pass now fails — and the diff between then and now is large.
You noticed a regression in production and you have a known-good release tag.
A bug is intermittent only on certain platforms but you have a script that can detect it.
You inherited a codebase and need to find when a strange behaviour started.
The interactive loop
Start a session
git bisect start git bisect bad # current HEAD is broken git bisect good v1.0 # v1.0 worked # Git now checks out a commit halfway between v1.0 and HEAD # Bisecting: 250 revisions left to test after this (roughly 8 steps) # [a1b2c3d] Refactor authentication middleware
Now you test the checked-out commit any way you like — run the app, run a unit test, click around in a browser. Then mark it:
git bisect good # this commit worked, move forward in time # or git bisect bad # this commit is broken, move backward in time # or git bisect skip # can't test this commit (won't build, etc.) # or git bisect reset # give up; restore HEAD to where you started
Repeat until Git announces the culprit:
The finishing line
b3a9f00a8c4d... is the first bad commit
commit b3a9f00a8c4d...
Author: Alice <alice@example.com>
Date: Wed May 14 09:00:00 2026 +0000
Tighten password rules
src/auth/login.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)Cleaning up
git bisect reset # ALWAYS end with this — restores your previous HEAD git bisect reset HEAD~5 # or reset to a specific commit
Automating with a script
If you can write a script that exits 0 for good and non-zero for bad, git bisect run will do the entire search hands-free.
A test script
#!/usr/bin/env bash # test-regression.sh — exit 0 if good, 1 if bad, 125 if untestable set -e npm install --silent || exit 125 npm test -- --testPathPattern=login || exit 1 exit 0
Wire it up
chmod +x test-regression.sh git bisect start HEAD v1.0 git bisect run ./test-regression.sh
What you'll see scroll by
Bisecting: 250 revisions left to test after this (roughly 8 steps) running ./test-regression.sh Bisecting: 125 revisions left to test after this (roughly 7 steps) running ./test-regression.sh ... b3a9f00a8c4d... is the first bad commit
Exit code 125 is special — it tells bisect "this commit can't be tested, skip it." Use that for commits that don't build at all.
Real-world workflow recipe
It's Monday. Yesterday's release broke something. The last release was tagged v3.4.0 last Tuesday, and HEAD is broken. Here's the full session:
The full bisect
# 1. Reproduce the bug locally on HEAD to make sure your test is real npm test -- --testPathPattern=checkout # ✗ test fails — good, we have a repro # 2. Verify v3.4.0 was clean git checkout v3.4.0 npm test -- --testPathPattern=checkout # ✓ passes — good # 3. Start the bisect git checkout main git bisect start git bisect bad # HEAD git bisect good v3.4.0 # last known good # 4. Either drive it manually... npm test -- --testPathPattern=checkout && git bisect good || git bisect bad # ...and repeat 8-ish times. # 5. ...or automate it git bisect run sh -c 'npm install --silent && npm test -- --testPathPattern=checkout' # 6. Bisect finds the culprit. Read it: git show b3a9f00 # 7. Always end with reset git bisect reset
Visualising the bisect
Each step roughly halves the candidate range. For N commits between good and bad, you need about log2(N) tests:
Steps versus commits
commits approximate tests
10 4
100 7
1,000 10
10,000 14
100,000 17You can also see the current state mid-bisect:
git bisect log # everything you've marked so far git bisect visualize # gitk-style view of remaining candidates git bisect view # alias for visualize
Replaying a bisect
# Save and re-run a session — handy for sharing or scripting git bisect log > my-bisect.log git bisect reset git bisect replay my-bisect.log
Custom terminology
Sometimes "good" and "bad" don't fit — for example when searching for the commit that fixed a bug. Rename the terms:
git bisect start --term-old=broken --term-new=fixed git bisect fixed git bisect broken v1.0 # now use: git bisect fixed / git bisect broken