OpenQodex 0.11.0 实测:审阅没有完成,为什么 Git push 还可能成功?
把 AI 代码审阅放进 Git pre-push Hook,听起来是一个很自然的主意:代码准备推送时,先让工具找出问题;没审过或者发现严重问题,就不让它进远端。
我最近拿 OpenQodex 0.11.0 做了几轮隔离测试。结果比“安装一个 Hook 就行”复杂一点:ShellCheck 确实能在修改过的脚本里抓到一个故意留下的错误;Git Hook 也确实能拒绝未经审阅的推送。但当独立模型审阅没能完成时,门禁并不一定会拒绝这次 push。
这不是说 OpenQodex 的 AI 审阅质量差——那一部分根本没有成功验收。值得先弄清楚的是,这个工具实际提供了三个不同的环节:静态扫描、独立 Reviewer、以及只查审阅记录的推送门禁。它们之间没有想象中那么自动。
ShellCheck 能检出错误,前提是它真的运行了
先只测扫描器。我在一个可销毁的 Git 仓库里准备了两版 Shell 脚本:基线版本对变量加了双引号,待检查版本故意删掉引号,制造 ShellCheck 的 SC2086。
这轮测试固定使用 OpenQodex 0.11.0,调用的是扫描命令,不包含模型 Reviewer。实际运行脚本里的核心参数是:
openqodex scan --uncommitted --only shellcheck --block-on-severity nitpick
这里为了让命令容易阅读,省略了测试脚本使用的 --cwd、--report-dir 和固定安装包的调用路径;不是声称实验逐字执行了上面这一行。要复现时,先在临时 Git 仓库里准备一份有实际 diff 的脚本,再使用固定版本的 CLI。完整语义见官方扫描命令文档。
同一份合成改动,离线禁装模式与可下载安装扫描器的模式,得到的结果不同:
| 条件 | 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 的分支也随之更新。
然后增加配置:
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 源码 看得更直接: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 的固定版本代码中,当收到与当前变更匹配、类型为 incomplete 的记录时,checkPush() 返回 abstain,意思是不主动阻止推送。这个分支没有因为配置了 block_on_severity: major 就改为拒绝。
还有另一层: hook.ts 生成的 Git Hook只在子命令返回 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”之前,最好先确认失败时究竟是谁说了算。