Git Bisect: The Bug-Hunting Superpower That Found a 2-Year-Old Flaw in 5 Minutes 🕵️
Git Bisect: The Bug-Hunting Superpower That Found a 2-Year-Old Flaw in 5 Minutes 🕵️
Last year, someone filed a bug report on one of my open source PHP libraries.
The issue: a specific edge case in the authentication token validation was silently returning true when it should have thrown an exception. Classic security-adjacent problem. Not exploitable in isolation, but the kind of thing that makes you sweat.
My reaction went in stages:
- "This is clearly a recent commit. Someone introduced this."
- "I'll run
git blameand find the culprit." - (finds own name) "Okay. I'll use
git bisectinstead. Like an adult."
git bisect found the exact commit that introduced the bug in four minutes. The commit was 14 months old. The commit message was mine. The commit message said "refactor: clean up token validation logic (no functional changes)."
Reader, there were functional changes.
What Is Git Bisect, Exactly? 🔍
git bisect is a built-in Git command that performs a binary search through your commit history to find the exact commit that introduced a bug.
You tell it two things:
- A bad commit (where the bug exists — usually now)
- A good commit (where the bug didn't exist — somewhere in the past)
Git then checks out commits in the middle, asks "good or bad?", and narrows down to the offending commit in O(log n) steps. For a repo with 1,000 commits between good and bad? About 10 steps.
git bisect start
git bisect bad # current commit is broken
git bisect good v2.3.0 # tag from 6 months ago, bug didn't exist
# Git checks out a commit in the middle
# You test it
git bisect good # or: git bisect bad
# Git moves to the next midpoint
# Repeat until...
# "Bisecting: 0 revisions left to test after this (roughly 0 steps)"
# Found: abc123def is the first bad commit
That's it. No dark magic. No expensive profilers. Just binary search applied to Git history. Beautiful.
The Story That Made Me Evangelize This Tool 📖
As a full-time developer who contributes to open source, I spend a lot of time investigating "when did this break?" issues in public repositories. It's one of the most valuable things contributors can do — not just submitting features, but tracking down regressions that maintainers don't have time to investigate.
About two years into my open source journey, I was helping triage issues on a PHP security library I used at work. A user had filed a detailed report: "Hash comparison function returns incorrect results for inputs over 4096 bytes."
The maintainer had pinned it as "confirmed, regression, unknown origin." The repo had 1,847 commits between the last known-good release and the current broken state.
My old approach: read through recent commits, make educated guesses, pray.
My new approach: git bisect.
git bisect start
git bisect bad HEAD
git bisect good v3.1.2 # Last known-good release
# Git: "Bisecting: 923 revisions left to test after this (roughly 10 steps)"
Ten steps. For 923 commits. I ran the test case manually each time. About 15 minutes total.
# Git checked out commit #461 (midpoint)
# I ran: php test.php "input longer than 4096 chars"
# Result: broken
git bisect bad
# Git checked out commit #230 (new midpoint)
# I ran the test again
# Result: working!
git bisect good
# ... repeated 8 more times ...
# Git: "a4f2c91 is the first bad commit"
The culprit: a performance optimization that changed how the function chunked large inputs. The author had tested with "Hello, World!" and not with anything over 100 bytes. Easy mistake. Fixed in a PR that afternoon.
The maintainer's response in the PR: "This is exactly the kind of contribution that keeps projects healthy. Thank you."
That feedback hit different. I'd found a bug that had been hiding for three months without touching any of the library's source code in a text editor. Pure Git archaeology.
Automating Bisect: The Part That Makes It Magical 🤖
Manual bisect is good. Automated bisect is sorcery.
If you can write a script that exits with 0 for "good" and 1 for "bad," Git will run the entire bisect automatically:
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
git bisect run ./test-the-bug.sh
Git runs your script at each midpoint and makes the good/bad decision itself. You walk away. You come back to a result.
Here's a real example from a Node.js project I was debugging:
#!/bin/bash
# test-the-bug.sh
npm test -- --grep "should reject tokens over 512 bytes" 2>/dev/null
exit $?
git bisect run ./test-the-bug.sh
# ...
# a7b3c9d is the first bad commit
# Author: Some Developer <[email protected]>
# Date: Mon Oct 14 16:23:11 2024
#
# perf: optimize buffer allocation in token parser
Automated bisect on a project with 500 commits in the range: about 45 seconds.
In the security community, we use this for a particularly interesting case: tracking when a CVE was introduced. When a vulnerability is disclosed, you can bisect to find the exact commit — which tells you which versions are affected, who wrote it, and what the fix looks like. That's actionable intelligence for a patch release.
Real-World Scenarios Where Bisect Saved Me 🏆
Scenario 1: "It worked fine in production last week"
Classic. Someone upgrades the staging environment. Something breaks. The staging environment has 300 commits of difference from production.
git bisect start
git bisect bad staging-sha
git bisect good production-sha
git bisect run npm test
Found the problem commit in 8 steps. It was a dependency version bump in package.json that silently changed behavior. The commit message was "chore: update dependencies."
Lesson: "chore: update dependencies" should be its own crime category.
Scenario 2: Helping an Open Source Maintainer
A Laravel package I contribute to had a report: "JSON responses are missing nested relationships after upgrading from 4.1 to 4.4."
Three minor versions. Dozens of commits. Maintainer was swamped.
I cloned the repo, wrote a 15-line PHP test script that reproduced the bug, and ran bisect with it:
<?php
// test-bug.php
require 'vendor/autoload.php';
// ... set up the model relationship ...
$result = $model->toJson();
$decoded = json_decode($result, true);
exit(isset($decoded['nested']['field']) ? 0 : 1);
git bisect run php test-bug.php
Done. Filed the PR with the fix and a link to the bisect output showing the exact bad commit. The maintainer merged it within an hour and said it was the most complete bug report they'd received all year.
Balancing work and open source taught me: a well-investigated bug report is worth ten "it doesn't work" issues. Bisect is what makes deep investigation fast enough to actually do it.
Scenario 3: "Who broke the build?"
The most emotionally satisfying use case. The CI pipeline turns red. Everyone on the team starts making eye contact too deliberately. Nobody's talking.
git bisect start
git bisect bad HEAD
git bisect good main~20 # 20 commits ago, definitely green
git bisect run npm run build 2>&1 | grep -q "error" && exit 1 || exit 0
Bisect named names. The developer who broke the build had to buy the team coffee. Git bisect: accountability as a service.
The Commands You Actually Need 📋
# Start a bisect session
git bisect start
# Mark current commit as bad
git bisect bad
# Mark a known-good commit (tag, SHA, branch name)
git bisect good v1.2.3
# After testing each checkout, tell Git the result
git bisect good # this commit is fine
git bisect bad # this commit has the bug
# Skip a commit you can't test (flaky test, unrelated build failure)
git bisect skip
# See where you are in the bisect session
git bisect log
# Visualize the commits being tested
git bisect visualize
# Automate with a script
git bisect run ./my-test-script.sh
# IMPORTANT: Reset when done (or you'll be stuck on a detached HEAD)
git bisect reset
That last one matters. I have forgotten git bisect reset more than I'd like to admit. You end up in a detached HEAD state, wonder why your editor looks wrong, and spend 10 minutes confused before remembering.
Don't be me. Reset when you're done.
When Bisect Doesn't Work (And What to Do Instead) 🚧
Bisect isn't magic for every situation:
Flaky tests: If your test passes 70% of the time, bisect will give you garbage results. Fix your flaky tests first. (Or use git bisect skip aggressively, but results will be less precise.)
Merge commits: If your repo has a lot of merge commits, bisect might check out commits in a strange order. Usually fine, occasionally confusing.
Build failures at old commits: Old commits might not build with your current Node/PHP version. Solution: use Docker to create a consistent environment for the test script.
The bug requires multiple commits to reproduce: Bisect finds the first bad commit, but some bugs require a combination of changes. In that case, bisect gets you close but the final diagnosis requires manual investigation.
In the security community, we call these "N-day vulnerability analysis" problems. The CVE might have been introduced by commit A, but only became exploitable when commit B removed a protective check. Bisect finds A. Grepping the diff finds B. Both matter.
How to Get Started Contributing With Bisect 🚀
This is the hidden superpower for new open source contributors: you don't need to understand the entire codebase to contribute meaningfully.
- Find a "confirmed bug" issue — look for labels like
bug,regression,confirmed - Identify the last known-good version — often mentioned in the issue comments
- Write a minimal reproduction script — even 20 lines of code is enough
- Run bisect — let Git do the work
- File a PR or comment linking the exact bad commit
You've just contributed investigative work that saves the maintainer significant time. That's valuable regardless of whether you write a single line of application code.
Good "first bisect" repos to practice on:
- Any open source project you already use and have locally
- Laravel framework (excellent bisect-friendly test suite)
- Node.js popular packages with good test coverage
- Your own past projects (humbling but educational)
TL;DR — Git Bisect Is Worth Learning This Week 🎯
- What it is: Binary search through your Git history to find bug-introducing commits
- Manual mode:
git bisect start→ mark good/bad → test each checkout →git bisect reset - Automated mode:
git bisect run ./test.sh→ walk away → read result - Open source power move: Bisect + minimal repro script = extremely high-value bug report
- Security use: Find exactly which commit introduced a vulnerability and what versions are affected
- Don't forget:
git bisect resetwhen done, or enjoy your detached HEAD state
The bug in my PHP library that I mentioned at the top? After I bisected it and submitted the fix, I also added a test specifically for inputs over 4096 bytes to the test suite.
My "no functional changes" refactor commit from 14 months ago is now permanently memorialized in the repository's blame history. Future contributors can hover over that line and read my name.
Consider it my open source cautionary tale. Consider git bisect your way to avoid becoming someone else's. 😅
Used git bisect to solve something nasty? Share the war story with me on LinkedIn or GitHub — I genuinely want to hear about the wildest commit that bisect has fingered for you.
What's your go-to debugging tool when "it was working yesterday" hits? Drop it in the comments — I'm building a list.