# OpenQodex 0.11.0 实测：审阅没有完成，为什么 Git push 还可能成功？

> Canonical: https://vxi.me/posts/openqodex-prepush-review-fail-open/
> Locale: zh-cn
> Published: 2026-10-11
> Updated: 2026-10-11
> Markdown: https://vxi.me/posts/openqodex-prepush-review-fail-open.md

把 AI 代码审阅放进 Git pre-push Hook，听起来是一个很自然的主意：代码准备推送时，先让工具找出问题；没审过或者发现严重问题，就不让它进远端。

我最近拿 [OpenQodex](https://github.com/openqodex/openqodex) 0.11.0 做了几轮隔离测试。结果比“安装一个 Hook 就行”复杂一点：ShellCheck 确实能在修改过的脚本里抓到一个故意留下的错误；Git Hook 也确实能拒绝未经审阅的推送。但当独立模型审阅没能完成时，门禁并不一定会拒绝这次 push。

这不是说 OpenQodex 的 AI 审阅质量差——**那一部分根本没有成功验收**。值得先弄清楚的是，这个工具实际提供了三个不同的环节：静态扫描、独立 Reviewer、以及只查审阅记录的推送门禁。它们之间没有想象中那么自动。

## ShellCheck 能检出错误，前提是它真的运行了

先只测扫描器。我在一个可销毁的 Git 仓库里准备了两版 Shell 脚本：基线版本对变量加了双引号，待检查版本故意删掉引号，制造 ShellCheck 的 `SC2086`。

这轮测试固定使用 OpenQodex 0.11.0，调用的是扫描命令，不包含模型 Reviewer。实际运行脚本里的核心参数是：

```console
openqodex scan --uncommitted --only shellcheck --block-on-severity nitpick
```

这里为了让命令容易阅读，省略了测试脚本使用的 `--cwd`、`--report-dir` 和固定安装包的调用路径；**不是声称实验逐字执行了上面这一行**。要复现时，先在临时 Git 仓库里准备一份有实际 diff 的脚本，再使用固定版本的 CLI。完整语义见[官方扫描命令文档](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/docs/plumbing.md)。

同一份合成改动，离线禁装模式与可下载安装扫描器的模式，得到的结果不同：

| 条件 | ShellCheck 状态 | 最终 finding | 扫描命令退出码 |
| --- | --- | --- | --- |
| 离线且禁止安装 | `not_installed` | 0 | 0 |
| 允许安装并运行 | `ran` | 1（`SC2086`） | 1 |

这是 2026 年 10 月 10 日那次固定版本合成实验整理出来的脱敏结果。在线这轮的 `rawCount=1`、`keptCount=1`，最终报告也确实包含 `SC2086`；离线那轮没有装到 ShellCheck，所以没有执行扫描，不能把它的 `0 findings` 解释成“代码没有问题”。

还有一处挺容易漏看：`SC2086` 在这条映射里属于 `nitpick`。OpenQodex 默认的 `review.severity_threshold` 是 `minor`，如果只看默认可见报告，可能以为没有检出。我们之前就遇到过这一层过滤，改成在实验里显式使用 `--block-on-severity nitpick`，才把这条合成错误留在了最终结果中。

这让我比较在意报告里的两个字段：**扫描器有没有运行**，以及**原始发现经过严重程度和变更行过滤之后剩下什么**。单看命令退出 0，至少在“工具未安装”的情况下，会得出错误判断。

## 真正的 pre-push Hook 不会自动启动 AI

第二轮我直接让 Git 把代码推向一个本地 bare remote，避免触碰真实项目。

安装 OpenQodex 的 pre-push Hook 后，先提交一份尚未审阅的变更。在默认配置下，Hook 打印未审阅提示，Git push 成功，本地 bare remote 的分支也随之更新。

然后增加配置：

```yaml
version: 1
review:
  block_on_severity: major
```

再提交一份新的、仍未经过审阅的改动。相同的 Git 推送路径这次被拒绝，测试也确认本地 bare remote 的 `main` 保持在推送前的提交。两种行为都来自真正安装的 Git pre-push Hook，而不是我们手写一个返回 1 的假钩子。

| 合成推送场景 | 观察到的结果 |
| --- | --- |
| 默认配置、没有审阅记录 | 警告，但允许 push |
| 设置 `block_on_severity: major`、仍没有审阅记录 | 拒绝 push，远端 HEAD 未变化 |

注意，这轮测试只证明**没有审阅记录时**的默认与阈值行为。它并没有启动 Codex，也没有生成一份真正由独立 AI Reviewer 完成的审阅收据。

从 [Hook 源码](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/cli/src/commands/hook.ts) 看得更直接：pre-push 负责读取 Git 即将发送的范围，查找本机 OpenQodex HOME 里与该变更对应的审阅记录，最后决定是否拒绝。它自己不会在每一次 push 时重新运行扫描器，更不会自动拉起第二个模型去读代码。

所以 `scan` 完成、`review` 完成、`hook` 放行，必须分别验证，不能混成一句“已经通过 AI 检查”。

## Reviewer 失败，Hook 为什么反而可以放行？

真正麻烦的地方出现在第三轮。本机尝试启动独立 Codex Reviewer 时，子进程确实开始了请求，但返回 HTTP 401；OpenQodex 把结果标成了 `incomplete`，没有得到有效的完成审阅记录。因此，这轮**不能用于评价独立模型找 Bug 的能力，也不能证明有效 Receipt 与过期 Receipt 的完整生命周期**。

本机测试摘要还记录了一次值得警惕的现象：在先前未审阅推送被拒绝后，`incomplete` 审阅之后的另一次 push 返回了 0，并新建了远端分支。这里两次 push 的目标引用并不完全相同，而且本机原始报告还没有纳入可独立核验的证据包；所以我不把这两次的先后关系直接写成一条已经完整复现的因果链。

但 OpenQodex 0.11.0 的**源码规则本身很明确**。

[ `push-gate.ts` 的固定版本代码](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/core/src/push-gate.ts)中，当收到与当前变更匹配、类型为 `incomplete` 的记录时，`checkPush()` 返回 `abstain`，意思是不主动阻止推送。这个分支没有因为配置了 `block_on_severity: major` 就改为拒绝。

还有另一层：[ `hook.ts` 生成的 Git Hook](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/cli/src/commands/hook.ts)只在子命令返回 1 时拒绝 push。工具本身出现错误、启动失败等返回其他退出码时，外层脚本会退出 0。它的目的是避免审阅工具故障把开发者的 Git 工作彻底锁死，但结果就是：**配置了严重程度阈值，不等于配置了“审阅必须完整成功”的强制门禁**。

这是一个很重要的产品语义区别，并不需要把它夸大成已证实的任意代码绕过漏洞。维护者可以有意选择“审阅服务故障时放行”，开发者也可以希望“审阅失败时拒绝”。问题在于，如果团队以为前者实现了后者，就会建立错误的安全预期。

## 如果真要强制审阅，我会怎么接

我会先把 OpenQodex 当作开发者反馈工具，而不是唯一一道不可绕过的合并门禁。它的扫描器能给出具体规则和文件位置；Hook 能提示当前变更缺少审阅。但是否允许合并，应该由一条独立、可观察的 CI 检查决定，至少分清：

- 扫描器是否实际运行，还是 `not_installed`、跳过或执行错误。
- AI Reviewer 是否真正完成，而不是 `unavailable` 或 `incomplete`。
- 报告是否属于当前 commit / diff，阈值是否与当前策略一致。
- CI 自身失败时，受保护分支是否仍然要求通过相应的 required check。

这是一种**部署建议，不是本次已经完成的 GitHub 分支保护集成测试**。如果你的团队要求“任何不完整审阅都禁止合并”，就不能只依赖上面这份 local pre-push Hook 配置；需要自己验证 CI 是否具备真正的 fail-closed 语义。

对于个人开发仓库，我反倒不一定要求每次 push 都过模型。按需运行 `review`，把扫描结果作为快速反馈，再由可测试的 CI 负责关键门禁，可能更符合成本和开发节奏。前提是不要把“有 Hook”误读成“代码已经由独立 AI Reviewer 检查过”。

这次可确认的范围其实很窄：固定版本 OpenQodex 0.11.0 在合成 Shell 变更上检测到了 `SC2086`；隔离仓库中的 Git Hook 复现了默认警告和配置后拒绝未审阅推送的行为；固定版本源码还明确写着 `incomplete` 和某些 Hook 故障的放行路径。**独立模型审阅没有完整跑通**，不存在可信的模型质量、误报率、延迟或有效 Receipt round-trip 结论。

也正因为这一段没有跑通，我暂时不会把 OpenQodex 当成需要强制 AI 审阅的生产仓库的唯一门禁。它作为扫描与审阅入口仍有价值，但在决定“能不能 push”之前，最好先确认失败时究竟是谁说了算。
