You’re staring at a bug that definitely wasn’t there six months ago. The codebase has hundreds of commits since then. You could read through every one looking for the culprit - or you could let git bisect find it for you in five steps.

git bisect performs a binary search through your commit history. You tell it one good commit and one bad commit, and it checks out the midpoint for you to test. You say “good” or “bad”, it halves the range again (running a binary search) and in no time it tells you exactly which commit introduced the problem.

For 32 commits that’s 5 steps. For 1000 commits, it’s 10. For a million, 20. And better yet - if you’re time poor jump to the end - you can automate the test and let git find the commit for you.

The demo repository

For this article I generated a small demo repository containing a single bash script, example.sh, assembled over 32 commits. Somewhere along the way a typo is added to the “Times tables” output. Instead of printing:

Multiplication table (2x):

it prints:

Multiplicaton table (2x):

One letter dropped. Easy to miss in a code review (and also a kind of example of a hard-to-find bug - nothing stops running - there’s just a typo in the output). Let’s find exactly which commit introduced it.

Setting up the session

First, confirm the bug is present in the current state:

$ bash example.sh | grep -i "multiplicat"
Multiplicaton table (2x):
Multiplicaton table (3x):
Multiplicaton table (4x):
Multiplicaton table (5x):
Multiplicaton table (6x):

The typo is there. Now start the bisect session:

$ git bisect start
status: waiting for both good and bad commits

Mark the current commit (HEAD) as bad:

$ git bisect bad
status: waiting for good commit(s), bad commit known

For the good commit, we’ll use the very first commit in the repo. At that point the script just printed Hello, world! - no multiplication tables, so definitely no typo. We pass its hash:

$ git bisect good 8f8d3f8
Bisecting: 16 revisions left to test after this (roughly 4 steps)
[030c230] Add 2x multiplication table

Git has checked out a commit halfway between the known-good and known-bad ends of the history. It tells us how many revisions remain and roughly how many more steps we’ll need.

Step by step

Step 1 - commit 030c230

$ bash example.sh | grep -i "multiplicat"
Multiplicaton table (2x):

Typo present. This is bad:

$ git bisect bad
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[ea19402] Show day of week in date

Step 2 - commit ea19402

$ bash example.sh | grep -i "multiplicat"

No output - the multiplication section doesn’t exist yet in this commit. No bug:

$ git bisect good
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[04753da] Calculate sum in counting loop

Step 3 - commit 04753da

$ bash example.sh | grep -i "multiplicat"

Still no multiplication section. Good:

$ git bisect good
Bisecting: 1 revision left to test after this (roughly 1 step)
[966062e] Add footer separator

Step 4 - commit 966062e

$ bash example.sh | grep -i "multiplicat"

No multiplication section. Good:

$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4ca34a1] Add multiplication table section header

Step 5 - commit 4ca34a1

$ bash example.sh | grep -i "multiplicat"
Multiplication table (2x):

Correct spelling - no bug here:

$ git bisect good
030c23036e71963812dd536ae823fadef4433942 is the first bad commit
commit 030c23036e71963812dd536ae823fadef4433942
Author: Bradley Dean <bjdean@bjdean.id.au>
Date:   Thu Mar 12 13:19:01 2026 +1100

    Add 2x multiplication table

 example.sh | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

Five steps across 32 commits.

Inspecting the culprit

$ git show 030c230
-echo "Multiplication table (2x):"
+echo "Multiplicaton table (2x):"
+for i in $(seq 1 $MAX); do
+    echo "  2 x $i = $((2 * i))"
+done

There it is. The typo was introduced right when the multiplication table code was added. It would later be refactored into a print_table function, which is why the bug shows up in all table headers in the final version - they all inherited the original misspelling.

Clean up

Always finish with a reset to return HEAD to where it was before:

$ git bisect reset
Previous HEAD position was 4ca34a1 Add multiplication table section header
Switched to branch 'main'

Automating with git bisect run

If your test can be expressed as a command that exits 0 for good and non-zero for bad, you can hand the whole session to git. It will run the command at each step without any input from you:

$ git bisect start
$ git bisect bad
$ git bisect good 8f8d3f8
$ git bisect run bash -c 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'

Output:

Bisecting: 16 revisions left to test after this (roughly 4 steps)
[030c230] Add 2x multiplication table
running 'bash' '-c' 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[ea19402] Show day of week in date
running 'bash' '-c' 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[04753da] Calculate sum in counting loop
running 'bash' '-c' 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'
Bisecting: 1 revision left to test after this (roughly 1 step)
[966062e] Add footer separator
running 'bash' '-c' 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4ca34a1] Add multiplication table section header
running 'bash' '-c' 'bash example.sh | grep -q "Multiplicaton" && exit 1 || exit 0'
030c23036e71963812dd536ae823fadef4433942 is the first bad commit
...
bisect found first bad commit

The same five steps, no interaction needed.

For test suites this means you can kick off an automated bisect and come back when it’s done. The command just has to exit 0 (pass) or non-zero (fail) - your existing test runner likely already does that.

One caveat: if your test command exits with code 125, git treats that as “skip this commit” (neither good nor bad). That’s useful when a commit doesn’t compile or is otherwise untestable.

A few practical tips

Narrow your good commit. The further apart your good and bad commits, the more steps bisect needs. If you know roughly when a regression appeared - say, this sprint - mark the start of that sprint as good rather than the very first commit. Fewer commits in range means fewer steps.

Use git bisect skip. If a checked-out commit won’t build or crashes before you can test it, use git bisect skip instead of good or bad. Git will try an adjacent commit instead.

The log is yours. git bisect log prints the full session so far. You can save it, pipe it through git bisect replay to redo the session, or just review your work.

Don’t forget to reset. If you close the terminal mid-session or something goes wrong, git bisect reset gets you back to a clean state. The bisect state lives in .git/BISECT_* files so it persists across terminal sessions.

Alternate terms “old”/“new” instead of “good”/“bad”* Bisect doesn’t actually care about the error - it’s really about searching for a change of any sort. To avoid confusion the alternate terms “old”/“new” can be used instead of “good”/“bad” - for instance to find where performance improved it would be confusing to have to refer to that as “bad”. Instead of marking terms with git bisect good|bad use git bisect old|new, and if you need a reminder of which one you are using in this sesison run git bisect terms.

What a Savings!

Even adopting strong test-driven development methodologies can leave you with a lot of repetitive work identifying a big in a large code base.

Automating git bisect is almost so powerful to look like magic - a job that done manually (even if you adopted a binary search yourself) would take hours or days might run in seconds, with the added reliability of forcing you to define a formal test for what failure looks like (which in itself may help understand the problem you are solving).