git bisect finds the commit where a behavior changed by testing revisions between a known good commit and a known bad one. With a repeatable test, git bisect run can automate the search. An AI coding agent can help turn the symptom into that test, then explain the much smaller diff Git identifies.
Suppose a discount worked in the last release and is wrong today. Asking an agent to inspect the whole checkout system is one option. Establishing which commit changed the result gives the investigation a concrete starting point.
This tutorial walks through a regression in a small Python function. The Git workflow applies to other languages; the important part is a test command that reliably distinguishes the behavior you care about from an environment failure.
When git bisect is useful
You need a revision where the behavior works, one where it fails, and a way to distinguish them. On a simple linear history, halving the candidate range makes a search across hundreds of commits manageable. The Git book's debugging chapter explains the search and basic commands.
Verify both endpoints with the same input. "The release was green in CI" is not evidence that your newly discovered edge case passed there.
For our example, the contract is deliberately small:
File: src/discount.py
Function: discounted_total(subtotal_cents, percent)
Input: 10000 cents, 20 percent
Expected output: 8000 cents
Observed output on the broken revision: 6400 cents
This example uses a standalone function with no third-party dependencies. For a real application, account for the runtime, dependencies, test data, and build process needed by each historical revision.
Bisecting is less useful when the cause is outside Git: a changed remote API, a production data issue, or a nondeterministic test. First make the failure repeatable. Our guide to debugging with AI coding agents covers building that initial reproduction.
Use a separate worktree for the investigation
Bisect changes the checked-out revision repeatedly. Give it a working directory of its own, especially if another terminal or agent is editing the current branch.
Run these commands from your repository root. Choose a sibling directory name that does not already exist:
git status --short
git worktree add --detach ../checkout-bisect HEAD
cd ../checkout-bisect
A detached worktree starts at the specified commit without checking out a branch. It has its own working files while sharing the repository's Git objects and refs. Uncommitted edits from the original directory are not copied. See the official git worktree documentation and our worktree guide for AI development.
If the bug exists only in your uncommitted changes, compare those changes first: there is no historical commit for bisect to identify yet. Also remember that worktrees can still use the same databases, ports, or external services. Isolate those resources if your reproduction needs them.
Create a probe that measures one behavior
Keep the probe outside the checkout so historical revisions cannot replace it. Create a temporary directory:
mktemp -d /tmp/discount-probe.XXXXXX
Save the following as check_discount.py in the returned directory. In later commands, replace /absolute/path/to/check_discount.py with that file's full path. Run it from the bisect worktree root.
import importlib.util
import sys
import traceback
from pathlib import Path
def main():
source = Path.cwd() / "src" / "discount.py"
if not source.is_file():
print("SKIP: src/discount.py does not exist at this revision")
return 125
# Avoid writing bytecode into the historical checkout.
sys.dont_write_bytecode = True
try:
spec = importlib.util.spec_from_file_location("discount_probe", source)
if spec is None or spec.loader is None:
raise RuntimeError("Cannot load the discount module")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
actual = module.discounted_total(10000, 20)
except Exception:
traceback.print_exc()
print("ABORT: inspect the probe or environment before continuing")
return 128
expected = 8000
print(f"discounted_total(10000, 20): expected {expected}, got {actual!r}")
return 0 if actual == expected else 1
if __name__ == "__main__":
sys.exit(main())
The Python standard library documents loading a source file directly with importlib. This probe is for the standalone module described above. A package with relative imports or application startup requirements needs a probe using that project's normal import and test setup.
The exception handler is intentional. An import failure or changed function signature deserves investigation; it does not automatically prove that the discount was applied twice. For this particular bug, only an unexpected returned total gets classified as bad.
Check the endpoints before starting the search
First confirm that today's revision gives the observed failure:
python3 /absolute/path/to/check_discount.py
It should print got 6400 and exit with status 1. Next, switch the isolated worktree to a release you believe is good. v1.4.0 is an example tag; substitute a real tag or commit from your repository:
git switch --detach v1.4.0
python3 /absolute/path/to/check_discount.py
That revision must produce 8000 and exit with status 0. If it also produces the wrong total, find an earlier working revision. If the probe aborts, resolve the test setup before assigning either label.
Now return to the original bad revision:
git switch --detach @{-1}
Here @{-1} refers to the checkout you just left. If you have switched elsewhere in between, use the explicit bad commit instead. The notation is documented in Git revision syntax.
Run the automated bisect
With the bad revision checked out and the good endpoint verified:
git bisect start HEAD v1.4.0 --
git bisect run python3 /absolute/path/to/check_discount.py
The order is bad revision first, good revision second. Git selects intermediate revisions and runs the probe. A successful search reports a first bad commit; an untestable range may leave several candidates.
The probe's exit code is the interface to Git:
| Exit code | Git's interpretation | Use in this probe |
|---|---|---|
0 |
Good | The total is 8000 |
1 to 127, except 125 |
Bad | The probe uses 1 for the wrong total |
125 |
Skip this revision | The example module is absent |
128 to 255 |
Abort the run | The probe uses 128 for an unexpected exception |
These rules, including the special skip code, come from the git bisect reference. Check that python3 and the probe run correctly before automation: shell errors such as a missing command can otherwise look like a bad revision.
If you use pytest instead, do not assume every nonzero result means the regression occurred. Pytest distinguishes failed tests, interruption, internal errors, invocation errors, and collecting no tests. Wrap it to map those outcomes deliberately; the official pytest exit-code reference lists them.
Give the agent the result and a narrow question
When Git reports a culprit, save its hash and inspect the patch:
git rev-parse HEAD
git show --stat HEAD
git show HEAD -- src/discount.py
git bisect log
Keep the hash and log with your task note. Then use a prompt like this:
The regression probe returns 8000 on the verified good endpoint
and 6400 on the bad endpoint. Git bisect identified the commit
shown below. The input is 10000 cents with a 20% discount.
Read the identified patch and the relevant caller.
Explain how the changed code could produce 6400.
Separate confirmed behavior from hypotheses.
Do not modify historical revisions during this investigation.
Propose a regression test and the smallest repair on the current branch.
The result 6400 suggests applying a 20 percent discount twice: 10000 × 0.8 × 0.8. That arithmetic gives the agent a specific hypothesis to check. It still needs to confirm where the second application happens.
A first bad commit is evidence about when the tested behavior changed. It may expose an older defect, and a merge may require inspecting both sides. Have the agent connect the changed code to the reproduction before choosing a fix.
Finish the investigation and verify the repair
After recording the result, exit bisect mode:
git bisect reset
This restores the checkout from before the bisect started. Keep the investigation worktree available until you have captured the evidence you need.
Implement the repair on your intended working branch. Add the regression case to the project's normal test suite, confirm that it fails without the repair, then verify the fix and adjacent behavior. For the discount example, adjacent cases include no discount, a full discount, and whatever rounding rule the application promises.
The external probe found the historical boundary. The permanent test should protect the application's real contract. See test-first workflows for AI agents for incorporating that check into the implementation loop.
Troubleshooting ambiguous results
If Git reports several possible commits, inspect which revisions were skipped. Try restoring their historical runtime or narrowing the verified range. Do not select a culprit just because it looks plausible.
If results change between runs on the same commit, stabilize the test's clock, random input, network responses, and data before continuing. Repeated runs can help diagnose flakiness; a majority vote does not establish a reliable history boundary by itself.
If the bug was introduced, fixed, and reintroduced, choose a narrower pair of endpoints around the occurrence you are investigating. A single search is not a complete history of every time the behavior changed.
If the probe keeps failing on imports, stop and adapt it to the project's historical layout. Treating setup errors as evidence of the discount bug will send the search toward the wrong change.
You should finish with four reviewable pieces of evidence: the reproduction, the verified endpoints, the identified change, and a permanent test for the repair. That is a much stronger starting point for an AI-assisted fix than a guess based on the current code alone.