You ran git bisect to find when a regression was introduced. The binary search narrowed down to a window where every candidate commit is untestable — the tree does not build there because of a refactor, the test framework changed, or the historical lockfile no longer installs. You keep typing git bisect skip, and eventually Git gives up with:
There are only 'skip'ped commits left to test.
The first bad commit could be any of:
abc1234...
def5678...
ghi9012...
We cannot bisect more!
That is not a crash — it is Git telling you it ran out of testable commits and can no longer narrow the suspects to one SHA.
Fastest fix (most cases): the commits are not really untestable, they just fail to build. Stop answering by hand and let git bisect run mark only true build failures as skip via exit code 125, while still judging the ones that do build:
git bisect run sh -c 'npm run build >/dev/null 2>&1 || exit 125; npm test -- -t "regression"'
If the whole window genuinely cannot build (a big-bang refactor), skip the entire range at once with git bisect skip <good>..<bad-window-end> and bisect the remaining linear history. The rest of this guide covers how to tell which situation you are in and how to recover the bisect.
Which situation are you in?
Run these three commands first; they tell you which fix applies.
| Check | Command | What it means |
|---|---|---|
| How many skips? | git bisect log | grep -c skip | A long run of skip before the stuck message = a build-broken window |
| Are good/bad reversed? | git log --oneline <good>..<bad> | Empty output = your good is newer than bad; the range is backwards |
| Is the test flaky? | Re-run the test 3x on one skipped commit | Different results each run = a flaky test is being misread as untestable |
Common causes
Ordered by hit rate, highest first.
1. A big-bang refactor left a build-broken window
A refactor landed as one (or a few) commits that do not compile mid-way, so 5-15 commits in the middle of the range all fail to build and every candidate becomes a skip.
How to spot it: git bisect log shows a long run of skip entries right before the stuck message, and git log --oneline <good>..<bad> shows a single huge “refactor” commit near the dead zone.
2. The historical lockfile or toolchain no longer installs
This is the most common false skip. The commit builds fine in its own era, but package-lock.json / Cargo.lock / go.sum pins versions that your current Node, Python, or compiler refuses to install today. The build fails for environment reasons, not because the code is bad, so you skip a commit that was actually testable.
How to spot it: the install/build error mentions a peer-dependency or engine mismatch, a removed registry package, or an unsupported language version — not your code. Compare node --version (or python --version, etc.) against what the project used when the commit landed; a jump like Node 18 to 22 is a classic trigger.
3. The test framework changed inside the range
The bisect window spans a test-runner migration (for example Mocha to Vitest, or Jest config moving to ESM). The test command you are passing to bisect run does not exist at the older commits, so the script bails out and the commit is skipped.
How to spot it: git show <older-candidate>:package.json shows a different (or missing) scripts.test. A command not found (exit 127) from your test step is the tell.
4. The test is flaky and randomly returns 125
In automated mode, a flaky test that occasionally fails to start makes git bisect run mark perfectly testable commits as skip. Those false skips accumulate around the real culprit and produce the “could be any of” list.
How to spot it: check out one of the skipped commits and run the test three times by hand. Inconsistent results mean the test, not the commit, is the problem.
5. The regression came in via a merge commit
A merge commit is in the range, and the bug was introduced by the conflict resolution inside the merge itself. Bisect can land on the merge but cannot test a meaningful intermediate state.
How to spot it: git bisect log | grep -i merge, or git log --graph --oneline <good>..<bad> showing merges clustered in the skipped region.
6. good and bad are reversed (or the range is non-linear)
If you marked the buggy commit good (or the old/new order is flipped), bisect searches the wrong direction and everything it hits looks irrelevant, so you skip your way into a dead end.
How to spot it: git log --oneline <good>..<bad> returns nothing — that means bad is an ancestor of good, so the range is empty or backwards.
Shortest path to fix
Step 1: Inspect the current bisect state before touching it
git bisect log # full session: every good/bad/skip so far
git bisect log > /tmp/bisect-backup.log # save it — reset wipes this
git bisect visualize --oneline # remaining suspects (alias: git bisect view)
git bisect visualize opens gitk when a graphical session is detected (via DISPLAY, SESSIONNAME, MSYSTEM, or macOS SECURITYSESSIONID) and falls back to git log otherwise. Adding --oneline or --stat forces the textual view. Count how many remaining commits are truly untestable versus falsely skipped.
Step 2: Skip whole ranges instead of one commit at a time
If a contiguous span genuinely cannot be tested, skip it in one shot. Range notation is <start>..<end> where start is exclusive and end is inclusive:
# Skip everything AFTER good_sha up to AND INCLUDING bad_window_end:
git bisect skip good_sha..bad_window_end
To also skip the start commit of the range, list it separately:
git bisect skip good_sha good_sha..bad_window_end
Or skip specific individual SHAs:
git bisect skip abc1234 def5678 ghi9012
Step 3: Let bisect run mark untestable commits automatically
Hand-answering is slow and is what produces accidental skips. Write a script that returns the exact exit codes Git expects:
0= good / old (regression absent)1-124and126-127= bad / new (regression present)125= untestable, skip this commit- anything else (especially
>= 128) = abort the whole bisect
125 is the magic skip code because POSIX shells reserve 126 (found but not executable) and 127 (command not found).
#!/bin/sh
# bisect-test.sh — exit 125 means "can't test here, skip"
# Build first; a failed build is untestable, not bad:
npm run build >/dev/null 2>&1 || exit 125
# Run the one specific test for the regression:
npm test -- --testNamePattern="performance regression" >/dev/null 2>&1
code=$?
[ "$code" -eq 127 ] && exit 125 # test runner missing in old commit = skip
exit "$code" # 0 = good, nonzero = bad
chmod +x bisect-test.sh
git bisect run ./bisect-test.sh
Behind the scenes, exit 125 calls git bisect skip for you, so a clean run never skips a commit that actually builds and tests.
Step 4: Fix false skips at the source
If the skips come from the environment (cause 2) rather than the code, restore a compatible toolchain so those commits become testable again — that often collapses the suspect list to one SHA:
# Switch Node to the version the project used at this commit:
nvm install 18 && nvm use 18
rm -rf node_modules
npm ci || npm install
npm test
Pin the same step inside bisect-test.sh (for example nvm use 18 or running the build inside a fixed Docker image) so every checkout uses the era-correct toolchain. For flaky tests (cause 4), retry inside the script before declaring a verdict instead of letting one bad run skip the commit:
# In bisect-test.sh: pass if any of 3 attempts succeeds
for i in 1 2 3; do
npm test -- --testNamePattern="regression" >/dev/null 2>&1 && exit 0
done
exit 1
Step 5: Use custom terms when good/bad does not fit
For a performance regression where the feature works but got slow, good/bad is misleading. Use your own terms:
git bisect start --term-old fast --term-new slow
git bisect fast <known-fast-sha>
git bisect slow HEAD
git bisect terms # confirm which term maps to old vs new at any time
Step 6: When Git still returns a range, inspect each candidate by hand
If the suspects are genuinely untestable and the list stands, examine each one:
for sha in abc1234 def5678 ghi9012; do
echo "=== $sha ==="
git show --stat "$sha"
done
Look for the commit that touches behavior-critical code — a config change, a dependency bump, or a subtle logic inversion. To re-run the search after correcting an earlier mistake, edit the saved log and replay it instead of starting over:
git bisect reset
# edit /tmp/bisect-backup.log to drop the wrong good/bad/skip line
git bisect replay /tmp/bisect-backup.log
Step 7: Clean up
git bisect reset # returns HEAD to the branch you were on before bisect
How to confirm it’s fixed
You have a real result, not another dead end, when:
git bisect run(or your last manual answer) prints a single line ending inis the first bad commit, followed by that commit’sgit showsummary — not the “could be any of” list.git bisect logends with onebadverdict immediately adjacent to agoodverdict (the two are parent/child), with no skip between them.- Checking out the reported commit and running your test reproduces the regression, while its parent (
<sha>~1) does not:
git checkout <first-bad-sha>~1 && npm test # should pass
git checkout <first-bad-sha> && npm test # should fail
Prevention
- Keep every commit on
mainbuildable. A green build check on every merge means a future bisect never hits a skip-everything window. Even WIP commits on feature branches should compile. - Drive bisects with a
bisect-test.shand exit code125, not interactive answers — it is faster and never accidentally skips a testable commit. - Pin the toolchain per commit. Build inside a fixed Docker image or read
.nvmrcin the script so historical lockfiles still install. - Tag known-good releases:
git tag good/v2.3.0, thengit bisect start HEAD good/v2.3.0starts with the smallest possible range. - Avoid big-bang refactors in one commit. Break them into independent, individually buildable steps.
- Save the log when you find the culprit (
git bisect log > bisect-finding.txt) so a teammate can replay it.
FAQ
Q: What do the git bisect run exit codes actually mean?
A: 0 = good/old (regression absent), 1-124 and 126-127 = bad/new (regression present), 125 = untestable (skip this commit), and any other code (including 128+ from signals) aborts the bisect. Never use 125 to mean “bad” in your script — it will silently skip instead.
Q: Git reported a range instead of one SHA. Is that a bug? A: No. When only skipped commits remain, Git cannot prove which one is first bad, so it lists every candidate and says “We cannot bisect more!”. Make those commits testable (fix the build/toolchain) and re-run, or inspect each candidate by hand.
Q: Does git bisect skip a..b include both ends?
A: No. The range is exclusive at the start and inclusive at the end, so a..b skips everything after a up to and including b, but not a itself. To also skip a, run git bisect skip a a..b.
Q: The first bad commit is a squash merge / a dependency bump. How do I find the real cause?
A: For a squash commit, bisect can only point at the squash; re-bisect the original feature branch history before it was squashed, or read its diff by hand. For a dependency bump, check that dependency’s changelog between the two versions and confirm by reinstalling the old version (npm install <dep>@<old-version>) and re-running the failing test.
Q: Can I bisect across multiple branches or a merge?
A: git bisect walks a single reachable history. Use git log --graph --oneline <good>..<bad> to see merge points, and if the bug is inside a merge’s conflict resolution, bisect the merged-in branch separately. Add git submodule update --init --recursive to your script if the test needs submodules at the matching commit.
Q: I made a wrong good/bad call halfway through. Do I have to start over?
A: No. Save the session with git bisect log > log.txt, edit out the wrong line, then git bisect reset and git bisect replay log.txt. This is also why you should back up the log before any reset — reset discards the in-progress session.