提交之前,我会先让 Codex 把这次 staged diff 骂一遍。如果 Claude 觉得它骂错了,就放行;如果两个都觉得有问题,提交就会被拦下来。
这套东西跑在 pre-commit 里,下面说说它怎么来的,以及三层里每一层在解决什么问题。
我为什么想把第一轮 review 往前移
不是说提交后的 review 不重要。相反,我还专门接了 argue bot 去处理飞书那一轮,那篇单独写过。
但有一类问题,我觉得不值得拖到提交后才第一次被看到:
- 从设计稿复制过来的图,还是原始未压缩的
- 我动了别人写的文件,其实只有纯格式化,结果混进了功能 commit
- staged diff 里有个比较明显的逻辑漏洞,但当时在想别的事没注意到
这类问题的共同点是:在本地最容易处理。真等代码提上去、报告出来,再切回来改,成本就高很多了。
所以我把这条线划得很清楚:
- 提交前:把机械问题和能在本地判断的明显风险先处理掉
- 提交后:再过一遍这次改动对别人的影响,CI 怎么说,PR 整体看起来合不合理
现在这个 pre-commit 主要就三层,顺序按确定性从高到低:图片资源、格式化拦截、adversarial review。
第一层:图片资源,不想手动做的事让脚本做
背景:以前是这么干的
我在做 Android 项目。图片放在 res/drawable,代码里用 R.drawable.icon_name,XML 里用 @drawable/icon_name。
设计稿在蓝湖上,切图直接下载是 png 或 jpg,未压缩的。以前要把一张图放进项目,流程是这样:
- 蓝湖切图,下载到本地
- 打开 squoosh.app,上传图片
- 有损和无损压缩各跑一遍,对比体积,手动选小的那个
- 看压完以后值不值得换成
webp - 再把最后选中的那个文件放进
res/drawable
每次都得做,繁琐但不容易出错。问题是每次加图都要走一遍这个流程,而且压缩这一步基本都得自己手动试:有损还是无损、压完到底小多少、值不值得换成 webp,甚至有些 case 压完以后 png 反而更小,最后还是得保留 png。
现在的做法
有了脚本以后,我做的只是一步:把蓝湖的图直接复制到 drawable 目录。剩下的让 pre-commit 来:
脚本只处理 staged 里的图片,.9.png、gif、动画 webp 这些会跳过。对可处理的图,同时跑有损(cwebp)和无损压缩,选更小的,改名为 _opt.webp,然后扫全部 kt、java、xml 文件,把资源引用从旧名字同步更新。
有一个特殊情况:如果我已经自己压缩过,放进来的是 webp 且文件名里带 _opt.,脚本就会直接跳过这张图。
命名冲突的情况——两张不同的图压完都变成同一个名字——脚本会直接阻断提交,不允许互相覆盖。
第二层:纯格式化,不想混进功能提交里
这一层完全没有 AI,反而是最朴素也最有必要的一层。
我用的 IDE 会在保存时自动格式化。有时候只是打开了一个别人写的文件改了几行,IDE 就顺手把整个文件的缩进、import 顺序全重排了一遍。这种改动混进功能 commit 以后,会把 git blame、review、问题追溯全搞脏。
所以 hook 会对 staged 里的 kt/java 文件做一个判断:
- 这个文件上一次提交不是我改的
- staged 内容去掉空白以后和
HEAD基本一样
就把它标记为"纯格式化",然后给我三个选择:继续提交(包含格式化)、把格式化文件从 staging 撤掉只提功能改动、或者取消这次 commit。
我后来发现这一步比想象中更值。很多时候根本没意识到 IDE 帮我动了别人的文件,直到这里才被提醒。
第三层:两个 AI 在 staged diff 上互相吵架
这一层才是这篇文章真正想说的。
先说 Codex 是什么
Codex 是 OpenAI 的代码 agent。有一个官方插件 openai/codex-plugin-cc,可以直接在 Claude Code 里安装:
/plugin install codex@openai-codex
装好以后,它提供了 /codex:adversarial-review——专门做"挑刺"式 review,质疑实现方向、压测假设、找潜在的崩溃路径和数据风险,不是找命名或格式问题。
我把它接进了 scripts/pre_commit_review.py,每次 git commit 时自动触发。
流程是这样的
- 取出 staged diff 里有实际改动的代码文件(
kt、java、py、js等) - Codex 做 adversarial review
- Codex 觉得没问题 → 直接放行,Claude 不介入
- Codex 提了问题 → 把问题交给 Claude,让它站到辩护方的位置看一遍
- Claude 认为 Codex 说的不成立 → 允许提交
- Claude 也确认有问题 → 阻断提交,输出两方的判断
review 的重点只有这几类:逻辑 bug、crash 风险、数据损坏、明显性能问题、安全漏洞。命名、注释、风格不在这一轮——那是提交后的事。
为什么是两个模型
两个原因。
第一,单模型容易误杀。Codex 有时候会对一些有意为之的取舍提出质疑,但那些取舍在当时的上下文里是合理的。Claude 作为辩护方,能用本地完整背景来反驳,比只让 Codex 一票否决更准确。
第二,互相兜底。Codex 顶不住的时候 Claude 还能接住;Claude 额度吃紧的时候 Codex 可以独立跑完整流程。两个模型都能各自单独完成这一轮,只是组合起来误报率更低。
我还专门给这一层做了缓存
没有缓存,这套东西很快就会让人想用 --no-verify 绕过去。
我现在是对每个文件的 diff 内容做 SHA256,取前 24 位作为 key,把 review 结果缓存到 .git/review-cache/ 里。只要代码没变,下次 commit 时直接命中缓存,不会重新调用模型;改了代码,hash 自然不同,缓存自动失效。
# 清除全部缓存(强制重新 review)
rm -rf .git/review-cache/
# 查看当前缓存了多少条
ls .git/review-cache/ | wc -l
缓存命中时,整个 pre-commit 额外加的时间接近零。没命中时,要等模型跑完,从几秒到几分钟,取决于 diff 大小。
所以我现在看 pre-commit 这一层,重点其实很明确:
- 图片资源这类机械活,尽量交给脚本处理
- 纯格式化这种会把 commit 搞脏的东西,尽量别混进去
- staged diff 里最容易漏掉的硬伤,先让模型在本地过一遍
这样等代码真的提上去以后,我面对的就不是一堆本来可以更早处理的小问题,而是提交后的那份 review 报告里真正值得看的东西。