Automating Regression Hunts with `git bisect run`
A test starts failing somewhere in the last two hundred commits. Instead of bisecting by hand, hand git a script and let it find the culprit while you do something else.

Someone pings you: "the totals endpoint is wrong, worked fine last sprint." You don't know which of the last hundred-odd commits broke it, and nobody wrote down when it last worked. The usual move is git bisect start, then git bisect good or git bisect bad by hand for every commit git hands you, rebuilding and re-testing each time. It works, but it eats your afternoon.
git bisect run does the same binary search without you in the loop. You give it a command that exits 0 for "good" and non-zero for "bad," and git drives the whole search itself.
Set the trap
Here's a small repo that stands in for the real thing: a shell function that adds two numbers.
# calc.sh
add() {
echo $(($1 + $2))
}A few commits later, someone "optimizes" it and quietly breaks it:
git log --oneline
# 8042654 add license
# b583da4 "optimize" add() with a bit shift trick
# c619324 expand README
# 844fbdb add README
# 40d2711 add test.sh
# f8157eb add calc.sh with add()Somewhere between the first commit and HEAD, add(2, 3) stopped returning 5. That's the whole bug report.
Write the check, not the search
bisect run needs one thing from you: a command that fails when the bug is present. A test script is perfect for this:
#!/bin/sh
# test.sh
. ./calc.sh
result=$(add 2 3)
if [ "$result" -eq 5 ]; then
exit 0
else
echo "expected 5, got $result"
exit 1
fiGive git the known-good and known-bad endpoints, then hand it the script:
git bisect start HEAD f8157eb
git bisect run ./test.shGit checks out the midpoint commit, runs test.sh, reads the exit code, and picks the next commit to check without asking you anything. Here's the real run, unedited:
Bisecting: 2 revisions left to test after this (roughly 1 step)
[844fbdb] add README
running './test.sh'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[b583da4] "optimize" add() with a bit shift trick
running './test.sh'
expected 5, got 6
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c619324] expand README
running './test.sh'
b583da4 is the first bad commit
commit b583da4
"optimize" add() with a bit shift trick
calc.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commitThree test runs, and git names the exact commit — no manual good/bad typing at all. Clean up the session with git bisect reset when you're done; it puts your working tree back where you started.
Where this earns its keep
A one-line shell check is a toy example. The pattern is what matters: swap test.sh for whatever command returns non-zero on failure.
- Unit tests:
go test ./... -run TestTotalsornpm test -- totals.spec.ts. - Build failures: a compile step that only started failing recently —
bisect run make buildfinds the commit that introduced the broken dependency. - Performance regressions: a script that runs a benchmark and exits non-zero if it's slower than a threshold. This is how you find the commit that made your API twice as slow without anyone noticing for three weeks.
A couple of things worth knowing before you point bisect run at a real problem:
- Exit code 125 means "skip." If a commit doesn't build at all, or the bug genuinely can't be tested there (a dependency didn't exist yet), have your script exit
125. Git skips that commit instead of treating it as good or bad, which would corrupt the search. - The script has to run standalone. It gets checked out fresh at every commit, so it can't depend on state left over from a previous run — no relying on a build cache from three commits ago, no assuming a file exists that a later commit deletes.
- It runs on your actual working tree. For anything with side effects — writing files, hitting a database — consider pointing the script at a throwaway copy or a
git worktree(more on that next week) rather than your main checkout.
The next time a regression shows up with no clue when it started, resist the urge to git blame your way through the diff by eye. Write the one check that tells git "good" from "bad," and let the binary search do the part computers are actually good at.