<!-- title: git bisect 配 Claude 自动揪出坏提交 -->

# git bisect 配 Claude 自动揪出坏提交：让无头模式当"语义法官"

## 概述

当你知道"线上坏了"，却不知道是过去几百次提交里的哪一次把代码改坏了，`git bisect` 是标准答案——但它需要一个能自动判定"这个版本好还是坏"的脚本。传统做法是手写一个复现用例，可一旦回归是"输出语气不对""布局错位""某个边界语义错了"这类难以量化的 bug，脚本根本写不出来。本教程教你把 Claude Code 的 `-p` 无头模式塞进 `git bisect run`，让大模型充当**语义法官**：每个被二分到的提交，自动读代码、按 bug 描述判定好坏、吐出结构化结论，最终无人值守地定位到那一个"罪魁祸首"提交。适合已熟悉 Git 基本操作、想把 Claude Code 接进 CI 与自动化排查流程的后端/前端工程师与 SRE。

## 前置条件

- **Git ≥ 2.30**：本机已安装 `git`，熟悉 `commit / log / checkout` 基本命令，对 `git bisect` 有概念性了解即可（本教程会带一遍）。
- **Claude Code ≥ v2.1**：已通过 `claude` 命令可用，已完成登录或配置了 `ANTHROPIC_API_KEY`。用 `claude --version` 确认。
- **bash + jq**：判定脚本用 `jq` 解析 JSON 输出，`jq --version` 能跑即可；Windows 用户建议在 Git Bash 或 WSL 下操作。
- **一个有回归的仓库**：最好是你手头真实的、能确认"某个旧 tag 是好的、当前 HEAD 是坏的"的项目。本教程也提供一个 5 分钟可造好的迷你案例用于练手。
- **预算意识**：二分大约需要 `log₂(N)` 次模型调用。100 个提交区间约 7 次调用、1000 个约 10 次，成本可控；但每次调用都会把当前代码读进上下文，大型仓库建议先用 `--add-dir` 或子目录限定范围。

---

## 正文

### 步骤 1：理解"二分 + 法官"的工作原理

`git bisect` 在好提交（good）和坏提交（bad）之间做二分搜索：每次 checkout 到中点，由你（或一个脚本）告诉它"这个版本是好是坏"，它再砍一半，直到锁定**第一个**引入回归的提交。`git bisect run <script>` 把"告诉它好坏"这一步交给脚本——脚本的退出码就是判词：

| 脚本退出码 | 含义 |
|---|---|
| `0` | 当前提交是 **good**（没有该 bug） |
| `1`–`124` | 当前提交是 **bad**（有该 bug） |
| `125` | **跳过**此提交（编不过、跑不了，无法判定） |
| `128` 及以上 | 中止整个 bisect |

关键洞察：**脚本内部用什么手段得出退出码，git 完全不关心。** 传统脚本跑一个测试套件，而我们要做的，是让脚本调用 `claude -p`——把"读代码 + 对照 bug 描述 + 下判词"这件事交给模型。这就是"语义法官"模式。

它的价值在"难量化回归"上才真正凸显：

- **能写断言的 bug**（如 `assert add(2,3)==5`）：直接写测试脚本，不必麻烦模型。
- **难写断言的 bug**：例如"导出的 CSV 表头顺序变了""错误提示从英文变成了中英混排""某类输入下生成的 SQL 漏了一个 JOIN"——手写正则或断言极其痛苦，但让模型读一眼代码就能判断。这正是 `-p` 法官的用武之地。

### 步骤 2：圈定 good 与 bad，写清楚"案情描述"

bisect 的输入是两个锚点。先确认它们：

```bash
# 当前 HEAD 已知是坏的（线上故障复现于此）
git checkout main
BAD=$(git rev-parse HEAD)

# 翻到一个你确信"还没坏"的旧版本，比如上个发布 tag
GOOD=v1.4.0
git log --oneline $GOOD..$BAD | wc -l   # 看看要二分的区间有多大
```

接着准备一份**案情描述**，放进 `bug-report.md`。写得越具体、越可验证，法官判得越准：

```markdown
# 回归案情

## 现象
调用 `grade.letter_grade(90)` 期望返回 "A"，但在 HEAD 上返回了 "B"。
score 恰好等于分级阈值（90/80/70/60）时，等级偏低一档。

## 判定标准（法官请据此判 good/bad）
- 阅读 src/grade.py 中的 letter_grade 函数。
- 若阈值的比较运算符是 `>=`（含等号），判 **good**。
- 若任一阈值写成 `>`（不含等号），判 **bad**。
- 只看 letter_grade 的阈值比较，不要被其它改动干扰。
```

> 常见问题：**区间太大、提交太多怎么办？** 先用 `git bisect visualize` 或 `git log --oneline` 看一眼，必要时用 `git bisect skip` 跳过明显无关的合并提交。区间每翻倍，调用次数只多一次，不必过度收窄。

### 步骤 3：编写调用 Claude 的判定脚本

新建 `judge.sh`，它的职责是：读当前 checkout 出来的代码 → 调 `claude -p` → 解析结论 → 退出 0/1/125。

```bash
#!/usr/bin/env bash
# judge.sh — 让 Claude 当语义法官，判定当前提交是 good 还是 bad
set -uo pipefail

REPORT="bug-report.md"

# --bare：CI/脚本推荐模式，跳过本地 hooks/skills/MCP/CLAUDE.md，
#         保证每台机器结果一致。认证必须来自 ANTHROPIC_API_KEY。
# --allowedTools "Read"：法官只需读代码，不给写权限，避免误改文件污染工作区。
# --output-format json + --json-schema：强制模型返回固定结构，便于 jq 解析。
OUT=$(claude --bare -p \
  --allowedTools "Read" \
  --output-format json \
  --json-schema '{
    "type":"object",
    "properties":{
      "verdict":{"type":"string","enum":["good","bad"]},
      "reason":{"type":"string"}
    },
    "required":["verdict","reason"]
  }' \
  "你是回归判定法官。请先用 Read 工具读取 src/grade.py，然后阅读 $(cat $REPORT) 中的判定标准。严格按照标准输出 verdict：阈值含等号=good，不含等号=bad。不要被其它改动干扰。" \
  2>/dev/null)

# 模型调用本身失败（额度、网络）→ 退出 125 跳过此提交，而不是误判
if [ -z "$OUT" ]; then
  echo "judge: empty response, skipping this commit" >&2
  exit 125
fi

VERDICT=$(echo "$OUT" | jq -r '.structured_output.verdict // empty' 2>/dev/null)
REASON=$(echo "$OUT" | jq -r '.structured_output.reason // "(no reason)"' 2>/dev/null)

case "$VERDICT" in
  good) echo "judge: GOOD  — $REASON"; exit 0 ;;
  bad)  echo "judge: BAD   — $REASON"; exit 1 ;;
  *)    echo "judge: AMBIGUOUS ($VERDICT), skipping — $REASON" >&2; exit 125 ;;
esac
```

几个关键设计：

1. **`--bare` 是脚本场景的推荐模式。** 官方文档明确：它会跳过自动发现的 hooks、skills、plugins、MCP 服务器和 `CLAUDE.md`，只认你显式传入的 flag。这意味着你队友 `~/.claude` 里某个奇怪的 hook 不会污染判定结果，CI 上每台机器行为一致。`--bare` 将成为 `-p` 的未来默认值。
2. **`--allowedTools "Read"`** 用的是权限规则语法，只给只读权限——法官不能改代码，否则它在某个提交上"顺手修一下"会毁掉工作区，让 bisect 的 checkout 失败。
3. **`--json-schema` 强制结构化输出**，结论落在 `.structured_output.verdict`。这是让"模型自由文本"变成"可解析判词"的关键，比正则匹配可靠得多。
4. **模型失败 → `exit 125`（跳过），而不是 `exit 1`。** 限流或网络抖动不该被当成"这个提交有 bug"。125 让 bisect 跳过当前点、继续推进，事后你可以对这些 skip 点单独复查。

> 常见问题：**法官判得不稳，同一段代码时好时坏怎么办？** 三招：(a) 把判定标准写到"非黑即白"——明确给出"看到 X 就是 good，看到 Y 就是 bad"，避免开放式判断；(b) 用 `--append-system-prompt "只依据判定标准，不要引入外部知识"`；(c) 把要判定的文件缩小到子目录，减少噪声。若仍不稳，可在脚本里对该提交连判 3 次取多数票。

### 步骤 4：让 git bisect run 全自动跑起来

赋予执行权限，启动二分：

```bash
chmod +x judge.sh

git bisect start
git bisect bad  "$BAD"
git bisect good "$GOOD"
git bisect run ./judge.sh
```

之后你就可以去倒杯咖啡。git 会反复 checkout、调用 `judge.sh`、根据退出码砍半。某次运行的输出大致长这样：

```
running ./judge.sh
judge: GOOD  — letter_grade uses >= for all thresholds
Bisecting: 37 revisions left to test after this (roughly 5 steps)
[abc1234] refactor grade thresholds
running ./judge.sh
judge: BAD   — line 12 uses > 90 instead of >= 90
Bisecting: 18 revisions left to test after this (roughly 4 steps)
...
def5678 is the first bad commit
commit def5678
Author: ...
    refactor: simplify grade thresholds
```

最后一行 `def5678 is the first bad commit` 就是凶手。看一下它改了什么：

```bash
git bisect reset          # 收尾，回到原来的分支
git show def5678          # 复盘这次回归到底动了哪几行
```

> 常见问题：**bisect 跑到一半，工作区被法官弄脏了？** 因为给了 `Read`-only 权限，理论上不会改文件；若你放宽了权限，`git bisect reset --hard` 可强制清回干净状态后重启。**跑到一半想换 bug 描述？** 直接改 `bug-report.md`，已判过的提交结论不会重算——若想全部重来，`git bisect reset` 再重新 start。

### 步骤 5：在 CI 里固化（--bare、续跑、成本可见）

把这套流程搬进 CI 时，重点是用 `--bare` 保证可重现，并捕获会话 ID 以便失败续跑。下面是一个可直接放进 `.github/workflows/bisect.yml`（或 GitLab CI、Jenkins）的骨架：

```yaml
name: auto-bisect
on:
  workflow_dispatch:
    inputs:
      good: { required: true }   # 例如 v1.4.0
jobs:
  bisect:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}   # --bare 不读 keychain，必须用环境变量
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }          # bisect 需要完整历史
      - run: chmod +x scripts/judge.sh
      - run: |
          git bisect start
          git bisect bad  ${{ github.sha }}
          git bisect good ${{ inputs.good }}
          git bisect run ./scripts/judge.sh
          git bisect reset
```

几个 CI 专属要点：

- **认证**：`--bare` 会跳过 OAuth 与 keychain 读取，认证必须来自 `ANTHROPIC_API_KEY`（或 `--settings` 里的 `apiKeyHelper`）。这是脚本模式最容易踩的坑。
- **成本可见**：`--output-format json` 的响应里带 `total_cost_usd` 和按模型的费用拆分。在 `judge.sh` 里顺手 `jq -r '.total_cost_usd'` 累加，跑完就能在日志里看到这次二分一共花了多少钱。
- **失败续跑**：若想在区间太大时分批跑，可在第一次调用时 `jq -r '.session_id'` 存下来，后续用 `--resume <session_id>` 让法官带着"我已经看过哪些代码"的记忆继续，省去重复读上下文的开销。
- **后台任务超时**：`claude -p` 里若起了后台 shell，约 5 秒宽限后会被终止；本场景法官只读不跑服务，不受影响，但如果你改成"法官自己跑测试再判"，记得测试要在前台完成。

### 步骤 6：用 Stop 钩子收尾通知与归档

bisect 跑完往往在 CI 或长脚本里，没人盯着。可以用 Claude Code 的 **`Stop` 钩子**做收尾：当一次 `-p` 调用结束，自动把"法官结论 + 凶手提交"推到 IM 或写进归档。

在项目 `.claude/settings.json` 里加（这是可提交、团队共享的位置）：

```json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/scripts/on-judge-done.sh"
          }
        ]
      }
    ]
  }
}
```

`on-judge-done.sh` 从 stdin 读 JSON（钩子输入字段在 stdin），提取结论：

```bash
#!/usr/bin/env bash
# on-judge-done.sh — 每次 -p 法官判定结束后触发
IN=$(cat)
REASON=$(echo "$IN" | jq -r '. // empty' 2>/dev/null)
# 这里可换成飞书/钉钉/Slack webhook，或追加写入 bisect 归档日志
echo "[$(date -u +%FT%TZ)] judge turn finished" >> "${CLAUDE_PROJECT_DIR}/bisect.log"
```

> 钩子配置分三层：`hook event`（如 `Stop`）→ `matcher group` → `hook handler`。`matcher` 为空表示对该事件的每次发生都触发。注意 `--bare` 模式**不会**读取这些钩子配置——所以"收尾通知"钩子要在交互式或非 bare 的编排脚本里才生效；纯 CI 的 `--bare` 调用想发通知，直接在 `judge.sh` 末尾 `curl` 一个 webhook 更直接。

---

## 实战案例：5 分钟造一个回归并自动定位

下面是一个从零开始、完整可复现的练手项目。我们造一个 `grade` 模块，故意在中间某次提交引入阈值 off-by-one 回归，然后用 `judge.sh` 自动揪出它。

**1) 建仓库 + 三个提交（前两个 good，第三个 bad）：**

```bash
mkdir bisect-lab && cd bisect-lab && git init -q
mkdir -p src

# commit 1 (good)：初版，阈值用 >=
cat > src/grade.py <<'PY'
def letter_grade(score):
    if score >= 90: return "A"
    if score >= 80: return "B"
    if score >= 70: return "C"
    if score >= 60: return "D"
    return "F"
PY
git add -A && git commit -qm "feat: add letter_grade"

# commit 2 (good)：加文档，逻辑不变
cat >> src/grade.py <<'PY'

# Convert a 0-100 numeric score into a letter grade.
PY
git add -A && git commit -qm "docs: document letter_grade"

GOOD=$(git rev-parse HEAD)

# commit 3 (bad)：重构时把第一个阈值改成了 > （引入回归！）
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("src/grade.py")
t = p.read_text().replace("score >= 90", "score > 90")
p.write_text(t)
PY
git add -A && git commit -qm "refactor: simplify grade thresholds"

BAD=$(git rev-parse HEAD)
echo "GOOD=$GOOD  BAD=$BAD"
```

**2) 写案情描述（沿用步骤 2 的 `bug-report.md`）：**

```bash
cat > bug-report.md <<'MD'
# 判定标准
- 阅读 src/grade.py 的 letter_grade。
- 所有阈值比较含 `>=`（等号）→ good。
- 任一阈值写成 `>`（无等号）→ bad。
- 只看阈值运算符，忽略其它改动。
MD
```

**3) 把步骤 3 的 `judge.sh` 放进项目，赋予执行权限。**

**4) 一键自动二分：**

```bash
git bisect start
git bisect bad  "$BAD"
git bisect good "$GOOD"
git bisect run ./judge.sh
```

**预期结果：** 法官在前两个提交上看到 `>=`，判 `GOOD`（exit 0）；在第三个提交上看到 `> 90`，判 `BAD`（exit 1）。git 大约 1–2 步即可锁定，最终输出：

```
<第三个提交的短 hash> is the first bad commit
```

**5) 复盘与收尾：**

```bash
git bisect reset
git show HEAD     # 确认就是那个 "refactor: simplify grade thresholds"
```

这个案例虽然小，但完整覆盖了"语义法官"的全部要素：**没有写任何针对 `letter_grade` 的断言测试**，纯靠模型读代码 + 标准描述就完成了定位。把它换成你项目里"CSV 表头顺序""错误信息措辞""生成 SQL 的 JOIN 缺失"这类难量化回归，套路一模一样。

---

## 工作流程图

```mermaid
flowchart TD
    A[确认 BAD=HEAD<br/>GOOD=旧 tag] --> B[编写 bug-report.md<br/>判定标准]
    B --> C[编写 judge.sh<br/>调用 claude --bare -p]
    C --> D[git bisect start<br/>good / bad / run judge.sh]
    D --> E{git checkout 中点提交}
    E --> F[judge.sh 读取当前代码]
    F --> G[claude -p 当法官<br/>--json-schema 强制 verdict]
    G --> H{verdict?}
    H -- good --> I[exit 0<br/>git 砍向更新的那一半]
    H -- bad --> J[exit 1<br/>git 砍向更旧的那一半]
    H -- 调用失败/模糊 --> K[exit 125<br/>跳过此提交]
    I --> L{区间收敛?}
    J --> L
    K --> L
    L -- 否 --> E
    L -- 是 --> M[输出 first bad commit<br/>git bisect reset]
    M --> N[Stop 钩子 / webhook<br/>归档通知]
```

---

## 常见问题与陷阱

- **法官在合并提交上犯迷糊。** 二分遇到 merge commit 时，工作区是两支线的混合，模型读到的代码可能"既是好的又是坏的"。对策：`git bisect skip` 跳过 merge，或在 `bug-report.md` 里明确"以主分支代码为准"。也可以先用 `git bisect start -- first-parent` 沿第一父链二分，避开合并噪音。
- **大型仓库上下文爆炸。** 每次调用都让模型读代码，万行文件会快速吃掉预算。在 `judge.sh` 里用 `--add-dir src/问题模块` 限定范围，或直接在 prompt 里点名"只读 src/grade.py"，别让模型满仓库乱翻。
- **判定标准写得太主观。** "看看代码是否合理"这种开放式描述会让模型给出不稳定的 verdict。务必把标准收敛成"看到特征 X = good / 特征 Y = bad"的判别式。标准越像开关，二分越像自动化。
- **混用了 `--bare` 又指望钩子生效。** `--bare` 不读任何本地配置，包括 `.claude/settings.json` 里的钩子。CI 里想要通知，要么用非 bare 调用，要么在 shell 脚本里直接 `curl` webhook。
- **把模型失败误当 bad。** 限流、超时、空响应必须 `exit 125` 跳过，绝不能 `exit 1`——否则一次网络抖动就会把一个无辜的好提交永久标记成"首个坏提交"，bisect 结论直接错位。

---

## 延伸阅读

- **Claude Code 官方 · 以编程方式运行**：`-p` / `--bare` / `--output-format` / `--json-schema` / `--allowedTools` / `--resume` 的权威说明，本教程所有 flag 语义均出自此处。
- **Claude Code 官方 · Hooks 参考**：`Stop` / `PostToolUse` / `Notification` 等事件的完整 schema、matcher 规则、JSON 输入输出与退出码语义。
- **Claude Code 官方 · 用 hooks 实现自动化**：相比参考页更偏 quickstart，适合第一次配钩子。
- **git 官方文档 · git-bisect**：`git bisect run` 的退出码契约（0/1-124/125/128）与 `--first-parent` 等高级用法。
- **社区方案对比**：Vibe Coding Skills 的 *bisect a regression with Claude* recipe、bestremotetools 的 *AI-Powered Git Bisect Automation*、以及 claude-devstudio 仓库里的 `skills/git-bisect/SKILL.md`，提供了不同的脚本组织方式，可借鉴其"AI 先生成测试再 bisect run"的变体思路（适合可自动化的 bug，与本文的"语义法官"互补）。

---

## 训练营适用性评估

- **最适合**：4–8 人的**工程效能 / SRE 进阶训练营**，或面向 2 年以上经验后端工程师的"AI 辅助排障"专题模块。学员需已会 Git 与 shell，能独立读懂 JSON。
- **教学时长**：讲授 + 跟练约 **90–120 分钟**。建议拆成两段：前 45 分钟讲原理 + 跑通迷你案例；后 45 分钟让学员拿自己仓库的真实回归练手，助教巡视判定标准的写法。
- **不太适合**：纯前端零基础营（git bisect 概念门槛偏高）、或以"产品/运营"为受众的低代码营。对这类受众，可退化成"用 claude -p 写一次性排查脚本"的轻量版本，去掉 bisect 编排。
- **可组合**：本模块可作为"Agent 模式与自动化"系列的一环，前置接"无头模式 JSON 输出"基础课，后接"无人值守 CI 巡检 Agent"——本教程的 `judge.sh` 正好是后者的判定内核。

---

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — `-p` / `--bare` / `--output-format` / `--json-schema` / `--allowedTools` / `--resume` 等 headless 模式 flag 的官方权威说明
2. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — `Stop` / `PostToolUse` / `Notification` 等钩子事件、matcher 规则、JSON 输入输出与退出码语义
3. [git-bisect — Git Documentation](https://git-scm.com/docs/git-bisect) — `git bisect run` 的退出码契约（0=good / 1-124=bad / 125=skip / 128=abort）与二分编排机制
4. [How to bisect a regression with Claude Code — Vibe Coding Skills](https://www.vibecodingskills.io/en/recipes/bisect-regression-with-claude) — 社区 bisect + Claude 实战 recipe，可对比"AI 生成测试再 run"的变体思路
5. [How to Build AI-Powered Git Bisect Automation — bestremotetools](https://bestremotetools.com/build-ai-powered-git-bisect-automation/) — AI 驱动 bisect 自动化的整体工作流与脚本组织参考

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
