Codex vs Claude Code:CLI 能力与修复验收方法
按官方资料核对两个本地 CLI,再用半开区间修复夹具练习行为与范围验收。真实模型比较尚未运行,速度、质量和费用结果留空。
- 比较本地 CLI 的工作流,IDE 与云端另设实验
- 审批、沙箱与项目指令都要核对实际加载配置
- 离线评估器已验证,真实模型质量、速度与费用未测试
1. 先确定这次比较回答什么问题
选编码代理时,最容易混在一起的是工具界面、模型能力和本机配置。一次修复成功,可能得益于更完整的项目说明;一次中途停住,也可能只是测试命令没有获批。把这些现象直接归结为某个产品更会写代码,会让下一次选型走偏。
这篇把比较范围收在两个本地 CLI:Codex CLI 与 Claude Code CLI。需要把仓库任务接进脚本时,可以先检查 Codex 的 exec 事件输出与权限配置是否吻合现有链路;团队已经维护 Claude 的项目指令、技能和钩子时,可以先验证 Claude Code,降低迁移成本。没有既有依赖,就用同一套仓库任务记录结果。后文给出的顺序属于接入建议,不能当成模型获胜结论。
证据核对于 2026 年 10 月 5 日。本机执行过 Codex CLI 0.159.2 的版本与帮助命令;未安装 Claude Code,它的语法依据官方文档核对。本文没有调用两家的真实模型,也没有五个真实任务的对照成绩。离线执行的是文末 Python 夹具及其评估器。速度、生成质量与费用栏均留空。
产品名称也要写完整。两家都提供终端之外的使用入口。IDE 内的选区、差异审阅,桌面端的会话管理,以及云端任务的环境和联网条件,都会改变操作过程。本文的 CLI 结论不外推到这些入口;需要它们时,单独建立一组比较。入口可参见 OpenAI CLI 参考 与 Claude Code 产品入口。
这篇可直接交付的是一套比较方法:用半开区间 merge_ranges 修复任务,把代码行为、修改范围和产品运行记录分开验收。离线练习需要 Linux 或 macOS、Python 3.10+ 和 Git;评估器会执行候选代码,先准备不含私人文件、凭据和生产挂载的一次性隔离环境。
| 路径与输入 | 阅读顺序 | 完成时核对什么 |
|---|---|---|
| 离线夹具:本页附录的 benchmark 文件 | 保存完整源码 → 运行评估器自测 → 查看半开区间任务和范围检查 | 原始实现被拒绝、人工参考实现通过,说明评估流程能区分这些案例;不填入任何产品成绩 |
| 真实比较:两份全新仓库、同一 prompt 与读者自己的授权账户 | 实验规约 → prepare 两份副本 → 新会话 → 停止后外部验收 | 九项行为测试、仅 ranges.py 的允许改动,以及版本、模型、权限、介入和日志都要留证;本篇两家结果仍待运行 |
| 迁移现有拒绝钩子:一次性标记目录与既有配置 | 单独读“迁移拒绝钩子时怎样验收” | 同时保留钩子命中、宿主处理和标记是否出现;代码修复夹具停用 hooks,不能代替这一验收 |
查看图示文字说明
Codex用codex交互、codex exec非交互;Claude Code用claude交互、claude -p非交互。两边都需记录过程,并在外部验证测试和diff。IDE与云端分开评估。
2. 交互与脚本入口怎样影响交付
交互模式适合问题尚未收敛的任务。人可以在代理读错范围时纠正,也可以在批准操作前检查命令。Codex 从 codex 启动;Claude Code 从 claude 启动。公平比较需要把追加提示记录下来,否则一边接受了多次指点,另一边独立做完,结果就很难解释。
脚本入口分别是 codex exec 和 claude -p。Codex 的 --json 输出 JSONL 事件,-o 可以单独保存最后一条消息。Claude Code 的 --output-format json 返回带结果和会话元数据的 JSON;需要逐事件处理时再选 stream-json,并按官方参考核对相应选项。两家的 JSON 形状不相同,不能共用一套字段解析器。见 OpenAI 非交互模式 与 Claude Code 非交互模式。
# Codex: read-only task with an event stream and final message.
codex exec --sandbox read-only --json \
-o codex-final.txt 'Read README.md and summarize it' \
> codex-events.jsonl 2> codex-stderr.txt
# Claude Code: deny calls that would need an unapproved permission.
claude -p 'Read README.md and summarize it' \
--permission-mode dontAsk --allowedTools 'Read' \
--output-format json > claude-result.json 2> claude-stderr.txt这两条命令仅演示接线方式,权限边界并不等价。实际自动化应分别保存标准输出、标准错误和进程退出码,再运行独立测试。代理进程正常退出,只能说明调用走到终点;回答里声称测试通过,还需要日志和重新执行的结果支撑。若达到预设时限,记录超时并保留当时的改动,别把半成品排除出统计。
3. 把授权方式和访问边界分开核对
Codex 把沙箱范围与审批策略分成两个设置。本机帮助列出的沙箱包含 read-only 和 workspace-write;审批策略包含 on-request 与 never。后者表示不再向人请求批准,不能据此推断所有命令都被允许。受限操作可能直接失败。官方非交互文档建议编辑任务明确使用 --sandbox workspace-write,新脚本也应避免沿用已弃用的 full-auto 快捷写法。见 OpenAI 非交互模式、OpenAI 审批与安全。
Claude Code 使用权限模式和工具规则。acceptEdits 会自动批准文件编辑及文档列出的部分文件系统操作;其它 shell 命令仍要核对规则。dontAsk 会拒绝原本需要询问的调用,已经允许的操作仍能执行。不能把它当作只读模式。shell sandbox 则是另一层,官方注明默认关闭,启用后约束 shell 命令及其子进程,覆盖范围与文件工具、MCP 等能力需要分别检查。见 Claude Code 权限、Claude Code 沙箱。
因此,比较表应写出允许读取、修改、联网和执行测试的具体范围。只记一个自动模式名称,会漏掉用户配置、组织策略及外部连接。实验仓库应放在没有私人文件、生产凭据和真实业务挂载的隔离环境里。模型服务需要连接供应商;被修复的样例本身不需要外网或安装依赖。
人工批准次数可以记录,但要同时记录请求内容和等待时长。明确拒绝了一次越界访问,可能正符合团队的操作规范。减少提示框的代价若是扩大访问范围,就不能直接记为效率提升。本文不使用绕过审批或移除沙箱的参数。
查看图示文字说明
左列是操作前审批,右列是运行时边界。Codex分别配置approval policy和sandbox mode。Claude Code以permission mode和工具规则审批,另可启用shell sandbox。模式名称不能直接一对一换算。
4. 项目指令和恢复会话要各自留证
Codex 在启动时构建项目指令链:全局说明与从项目根目录到当前目录的文件依次进入上下文,同一目录优先选择 AGENTS.override.md,再看 AGENTS.md。实验从哪里启动,因此会影响读到哪些规则。见 OpenAI AGENTS.md。
Claude Code 使用 CLAUDE.md,当前官方文档也支持直接读取 AGENTS.md。后者要求 v2.1.277 或更高版本,并受加载条件影响:默认情况下,工作目录或其上级存在项目 CLAUDE.md 或 CLAUDE.local.md 时,会优先读取 Claude 指令;设置、内置插件状态和部分版本限制也会影响结果。不能再简单写成两者都只需要 AGENTS.md,或 Claude 完全不认这个文件。见 Claude Code 项目指令与记忆。
文末样例把同一份规范分别写进 AGENTS.md 和 CLAUDE.md,让两种常见入口都能拿到相同任务。运行前仍要检查实际加载内容,并记录全局规则、自动记忆和组织设置。文件名相同、文本相同,都不足以保证最终上下文完全相同。
codex resume --last
claude --continue
claude --resume SESSION_ID恢复会话适合接着做工作,比较第一轮能力时则应新开会话。两家都有恢复入口,见 OpenAI CLI 参考、Claude Code CLI 参考;它们延续的对话不会替你证明当前分支、未提交文件和依赖仍与上次一致。接续任务前,先查看 Git 状态和最近的测试结果。压缩后的上下文也不应用来代替一份明确的交接说明。
查看图示文字说明
比较时相同规范分别写入两种原生指令文件,确认加载内容。Codex的AGENTS沿项目路径读取,Claude的CLAUDE有自己的加载规则。会话恢复与Git工作目录状态分开验证。
5. Skills hooks 和 MCP 应单独算一层
两家都能把重复流程放入 SKILL.md,也都支持 hooks 和 MCP。因此,仅用有没有扩展能力很难做选择。更实用的检查是:团队的技能放在哪,调用方式是否保留,脚本依赖怎样安装,工具权限能否按原来的范围约束。
Codex 的仓库技能常见位置是 .agents/skills,Claude Code 是 .claude/skills;两者都采用按需加载技能内容的机制。基础技能文本可以复用,但涉及专有 frontmatter、工具名、参数替换或调用控制时,需要逐项核对,不能把整个目录搬过去就视为迁移完成。见 OpenAI Skills、Claude Code Skills。
Hooks 在事件发生时运行处理程序,适合把检查放进流程。Codex 当前文档提供 PreToolUse、Stop 等生命周期事件,并要求审阅信任非托管钩子;Claude Code 也提供 PreToolUse 等事件,返回格式与阻断规则另有定义。同一个事件名称不保证输入字段和失败行为一致。接入前应使用可控样例验证触发、拒绝和错误记录。见 OpenAI Hooks、Claude Code Hooks。
MCP 配置也有差异:Codex 的服务器配置位于 config.toml 的对应表,Claude Code 的项目服务器可放在 .mcp.json。尤其要注意 Claude 的非交互运行不会出现交互式项目服务器批准提示,官方文档建议显式控制配置来源。每个连接器都可能改变任务可见的数据和可执行的动作。基础比较先停用无关扩展,之后再单独测团队真实配置。见 OpenAI MCP、Claude Code MCP。
6. 迁移拒绝钩子时怎样验收
现有修复夹具明确停用 hooks,能检查代码交付,无法证明团队的拒绝钩子迁移成功。迁移时应另开一组验收,只用一次性目录里的无害标记文件,先确认工具确实能写入,再测试钩子能否阻止同一个动作。
这里有一个具体差异。核对于 2026 年 10 月 5 日的 OpenAI Hooks 参考说明,PreToolUse 返回 continue: false、stopReason 或 permissionDecision: "ask" 会被记为 hook 失败,工具调用流程继续。Claude Code Hooks 则支持停止处理与请求确认的相应语义。需要拒绝当前工具调用时,两边都列出了下面的结构。不要把旧钩子的整段返回值直接搬过去,也不要把“流程继续”写成“无条件执行”:审批、沙箱和组织策略仍需各自核对。旧式 decision: "block" 在两边参考中仍可用,不能按字段新旧一概判断兼容性。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Migration check: keep the marker absent."
}
}验收对象先限定为本地 CLI 的同步命令 hook。每个案例使用新的目录和标记名,保留相同的正常权限设置。处理程序另记一条命中日志;工具只负责创建标记。记录实际工具名、事件输入、stdout、stderr、退出码或超时、宿主如何处理响应,以及标记最终是否存在。这样能区分钩子没加载、响应不合法和工具被其它权限层拒绝。
| 案例 | 钩子侧证据 | 工具侧证据与判定 |
|---|---|---|
| 正控制 | matcher 命中,处理程序正常结束但不返回决定 | 正常权限允许时标记应出现;否则先排查环境,此轮不能证明拒绝钩子有效 |
| 合法 deny | 命中日志、上方 JSON、宿主明确记录拒绝 | 同一写入被拒绝且标记不存在,才有完整拒绝证据 |
| 迁入的停止字段 | 单独测试 continue:false 与 stopReason 组合,保存宿主解析结果 | Codex 文档指向 hook 失败后继续流程;Claude 指向停止。不能把两种结果记成相同的 deny |
| 迁入的 ask | 单独测试 permissionDecision:ask,记录入口能否接收确认 | Codex 文档不支持;Claude 的交互确认与无人接收确认的非交互入口要分别记录 |
| 异常退出/坏 JSON | 分开制造退出 1 且无 stdout、退出 0 且输出损坏 JSON 两个案例 | 记录错误后是否继续,不能把错误提示计为拒绝成功;Codex 同步命令的这些分支留给目标版本验证 |
| 超时 | 处理程序命中但未在规定时间内完成,保留超时日志 | 记录后续工具状态;Claude 命令 hook 在默认 onFailure: "continue" 下继续正常权限流程;v2.1.295+ 可配置 onFailure: "block" 在此 PreToolUse 超时案例中阻止工具调用。Codex 此处不预填实测结果 |
| matcher 未命中 | 确认配置已加载,但该案例没有处理程序命中日志 | 正常权限允许时标记应出现;这是覆盖检查,不能算拒绝测试通过 |
异常和超时要按处理程序类型、事件及配置核对。Claude 官方文档说明,v2.1.295+ 的 command 或 HTTP hook 可设置 onFailure: "block";在 PreToolUse 事件中,启动失败、0 或 2 以外的退出码、超时或无效 JSON 等失败会阻止工具调用,默认值仍是 continue。验收时应分别记录默认配置与阻断配置,不能把两者合并成同一种失败行为。Claude 文档还区分命令 hook 与 Agent SDK callback 的超时行为;OpenAI 对远程 MCP 回调的故障说明,也不能直接充当本地命令 hook 的实测记录。迁移记录还应保存 CLI 版本、配置来源、脚本哈希及加载或信任状态。任何一项缺失,都先记为未完成验收。
附录的 hook_contract_check.py 只把已核实的少量响应字段转换成离线分支,并用临时标记文件检查判定代码。其中 Claude 命令 hook 的异常与超时分支只示意默认 onFailure: "continue",不覆盖 onFailure: "block"。异常与超时由固定输入模拟,没有启动或拖慢真正的 hook;未确认的分支输出 unresolved,对应标记结果为 null。它不连接模型、不读取或改写用户 hooks,也不实现完整宿主协议。离线检查通过后,真实宿主验收仍是待运行,不能据此宣布任一产品的安全边界更强。
python3 -B benchmark/hook_contract_check.py > hook-contract-results.json7. 先写实验规约再打开两个终端
实验单元是一份固定仓库、一个确定任务和一套验收规则。两家分别从全新副本出发,使用同一任务文本。模型要记录实际 ID、推理设置与 CLI 版本;若只能获得别名,注明别名会变化。操作系统、Python 和 Git 版本也一起保存。不能一边带着个人记忆和内部文档连接,另一边只有空白环境。
先规定一次尝试的结束点:代理交出最终答案,或到达事先写下的时限。记录墙钟时间,并单列批准等待。人补充错误位置、让它重试、手动修改代码,都算介入事件。第一次尝试先验收,再决定要不要继续修复;追加提示后的成功另记一轮。
正式比较时,为每个配置准备多个新副本,交替运行顺序,保留失败样本。这个小任务可用来检查流程是否可复现,任务难度不足以代表大型仓库。扩充任务集时,应选择团队实际会遇到的边界修复、跨文件修改和回归检查,并提前写好验收标准,避免看过结果后临时换评分口径。
故障记录至少分清三类:候选代码没有满足需求,属于任务结果;Python 或 Git 本身缺失,属于环境问题;测试命令因未授权而被拒绝,属于权限配置问题。三类都要保留,后两类经过修复再开新一轮,不能悄悄替换原记录。否则最终展示的通过率,会隐藏为让工具跑起来付出的工作。
本例采用公开测试,因此比较的是代理拿到明确验收后的修复过程。它不衡量面对未知需求时的泛化能力。需要隐藏测试时,应在开始前单独准备并锁定,给两边同样的可见信息;不能临时为某个实现增加特例,也不能把被测程序产生的答案拿来当裁判。
查看图示文字说明
实验固定代码、提示、验收、环境和权限,使用新会话,记录所有扩展和人工介入。模型精确ID与重复次数提前确定,避免把工具外壳、模型和配置混成一个变量。
8. 用半开区间修复任务验证比较流程
样例只有一个待修复函数 merge_ranges。输入是整数二元列表,表示半开区间;重叠区间要合并,仅在端点接触的区间保留分开。输出按起点排序,输入保持不变,空列表返回空列表,起点大于或等于终点时报 ValueError。其它类型校验不属于本题。
仓库里的实现故意含错,测试覆盖空输入、普通区间、端点相接、重叠、嵌套、乱序及不修改输入,还有两个无效区间。允许修改范围只有 ranges.py。任务禁止改测试和指令文件、提交 Git、联网安装依赖或使用额外代理;只需标准库 unittest。这样的范围足够小,读者能逐行审查结果。
python3 benchmark/prepare.py trial-codex
python3 benchmark/prepare.py trial-claude
# Check the base commit in both independent repositories.
git -C trial-codex rev-parse HEAD
git -C trial-claude rev-parse HEADprepare.py 使用固定身份、提交时间与文件内容创建仓库,拒绝覆盖已有目录。基线 commit 为 abf98a76e2fd74e99c1921bffb4ee0bf776bd63f,manifest.json 还保存了每个任务文件的 SHA-256。不同工具产生的最终改动都回到这份基线比较,不依赖代理自己描述改了什么。
完整源码在本页末尾的可展开附录中,按标签保存路径即可组成 benchmark 目录。人工参考实现单独存放,仅用于评估器自测,不能暴露给待比较的代理。它没有被标记为任何产品的生成结果。
9. 测试通过之后还要检查修改范围
评估器位于任务仓库外,读取可信测试副本,对候选 ranges.py 运行九项测试。测试前比较文件哈希,检查新增、删除和非许可文件修改,并确认基线分支引用未变。范围不通过就不执行候选模块;正常完成后输出 JSON 报告和统一 diff。模块导入和测试都有时间限制。
python3 benchmark/evaluate.py trial-codex --out codex-eval.json
python3 benchmark/evaluate.py trial-claude --out claude-eval.json评估命令退出 0 表示这九个公开测试及当前文件范围检查通过;退出 1 表示失败、超时或范围违规。这里的退出码属于评估器,与产品自身的进程退出码分开记录。它会执行候选 Python,无法充当安全沙箱;只应在无敏感数据的一次性环境中运行。隔离模式、禁止联网和禁止额外操作,需要外部环境或审计记录来证明。
文件检查发生在执行候选模块之前,不能检出运行期新增的越界写入。它忽略 .git 和 __pycache__ 的内容,只单独核对 HEAD 对应的固定分支引用。它看不到后来恢复的瞬时修改,也无法证明候选代码没有投机适配公开测试。因此,测试之后仍要人工审查 diff,确认逻辑覆盖任务,并检查运行日志有没有未经允许的行为。
本文实际验证了十项评估器自测:原始错误实现被拒绝,人工参考实现通过,修改测试、删除指令、新增文件、符号链接、改动分支引用及超时会被拒绝,两个新副本的 commit 相同,异常 JSON 汇总也会被拒绝。原始实现的九项行为测试中出现四个失败和一个错误;参考实现九项通过。这些数字只验证夹具。
查看图示文字说明
官方文档、本机帮助、离线夹具、真实模型实验是不同证据类别。本篇只有前三类,其中本机CLI证据限Codex。真实产品实验未做,没有速度、质量或费用排名。
汇总时,先写尝试总数和有效样本数,再写符合验收的次数。单个小任务偶尔成功,不能换算成稳定的产品能力。耗时差很小时,还要看机器负载、供应商等待和人工审批是否改变了结果;重复运行后仍有明显差异,才值得扩大任务集继续验证。
产品结果表保持待运行。空白表示没有数据,不能当作零耗时、零费用或零介入。run-record.template.json 用于保存版本、模型、权限、会话、人工介入与评估报告;费用若取自客户端估算,也应与实际账单分列,不能直接混成成本冠军。
| 工具 | 测试结果 | 修改范围 | 耗时 | 人工介入 |
|---|---|---|---|---|
| Codex CLI | 待运行 | 待运行 | 待记录 | 待记录 |
| Claude Code CLI | 待运行 | 待运行 | 待记录 | 待记录 |
10. 根据接入成本安排试用顺序
已经用 Codex exec 消费 JSONL、维护其沙箱配置,并以 Git diff 作为交付入口的团队,可以先用 Codex 完成试点。优先验证日志解析、权限拒绝以及中断后的接续操作。能复用现有设施,是一个明确的选择理由;要判断模型是否更适合仓库,还需补上真实任务数据。
已经积累 CLAUDE.md、Claude skills、hooks 和 MCP 配置的团队,可以先用 Claude Code 试点。检查这些扩展在目标运行入口里是否实际加载,以及非交互时是否仍保持预期限制。改用其它工具之前,应列出迁移项目,估算维护成本,避免只比较第一条命令的长度。
从零开始时,可先用本页小夹具排除接入和权限问题,再拿代表性仓库任务做对照。功能测试或修改范围不过关的结果,先进入故障分析;合格结果之间再比较人工介入、审阅难度和交付时间。若一个产品受账号政策、系统环境或预算限制不能使用,这些硬条件应直接写入选择记录。
费用比较还要保留账号口径:订阅中包含的使用额度,与按调用产生的账单并非同一种成本记录。若无法把账单准确分配到单个任务,就只报告可观察的用量和限制,不填一个看似精确的单次金额。修改模型或套餐后,旧结果应保留原来的日期和配置。
也可以保留两套工具,但要让各自的任务有清晰边界。给不同工具独立分支或工作副本,交接时附上任务说明、diff 和测试记录,避免同时覆盖同一份未提交改动。一次比较最有用的产物,是知道哪种配置适合哪类任务,以及失败时该检查哪里。
查看图示文字说明
已有Codex脚本与隔离配置可先试Codex;已有Claude项目扩展可先试Claude Code;无既定依赖就以相同验收比较代表性任务。政策和预算硬约束优先于偏好。
11. 从可复现的第一轮开始
第一次执行时,先运行评估器自测,确认本地 Git 与 Python 能复现基线。然后建立两个副本,在同样的隔离条件下启动新会话,粘贴 prompt.txt。模型选择和授权由读者按自己的账号与组织政策决定,本文没有代为启动、计费或批准任何真实调用。
# Offline harness verification.
(cd benchmark && python3 -B -m unittest -v test_evaluator)
# Launch separately, inside each prepared repository.
(cd trial-codex && codex --sandbox workspace-write \
--ask-for-approval on-request)
(cd trial-claude && claude --permission-mode default)代理结束后,把日志和人工记录保存在任务副本外,再运行外部验收。只统计有完整记录的尝试,同时列出因环境故障而无效的尝试及原因。需要第二轮时重新 prepare,保持失败证据和原始基线。随着 CLI 更新,先重新读取 help、核对权限文档,再更新实验记录里的版本。
查看图示文字说明
检查环境和授权后建立两个相同基线仓库,用新会话同提示运行。结束后保留日志和退出码,外部运行评估,再人工审查diff。下一轮重新建副本。
12. 再补一个跨文件任务:新 API 上线,旧调用继续可用
半开区间任务把问题收在一个函数里,适合确认修复与验收流程。实际迁移还会遇到另一种交付:新接口写好了,包入口却没有导出;或者旧函数名还在,已有脚本的关键字调用已经不能用了。下面只练习这类兼容边界,让两边处理同一组实现、公开入口和旧调用方。
例子是一个标签格式化小包。拼接逻辑刻意保持简单,审阅重点是参数约定与调用链,而非算法难度。所有输入和预期都已给出;错误版本与参考改法由人工编写,两家产品的实际表现仍待运行。这也不能代表大型项目的重构能力。
给两边的固定任务如下。原有的版本、模型、权限、初始会话和介入记录仍按前文执行,不额外假定任何 CLI 会自动发现调用方。
只允许修改 tagfmt/core.py 和 tagfmt/__init__.py。为这个标签格式化包新增 format_tags(values, *, separator=","),在包入口公开导出新旧两个函数,__all__ 仅含这两个名称。新 separator 参数只能按关键字传入。
旧 join_tags(tags, sep=",") 仍需兼容默认参数、第二个位置参数,以及 tags、sep 这两个旧关键字参数。不要修改 tagfmt/__main__.py 或 legacy_job.py 来迁就新接口。
输入限定为字符串列表与字符串分隔符;保持元素顺序、重复项和原有空白,不修改输入,空列表返回空字符串。不增加类型校验、弃用警告、排序或去重。
可以读取这四个输入文件、运行本目录内的 Python 命令及给定外部 contract_check.py 检查。不要联网、安装依赖、读取参考实现、修改检查程序、改写 Git 元数据或提交。完成后列出修改文件、新旧入口检查结果、没运行的检查和剩余风险。
13. 固定输入:同时保留包入口与两个旧调用方
把下面四个文件按路径保存,便得到完整输入。分别复制到两个全新目录,第一轮都从这四份原文开始。仅允许改 tagfmt/core.py 与 tagfmt/__init__.py;另外两个文件是必须继续工作的现有消费方。
def join_tags(tags, sep=","):
return sep.join(tags)
from .core import join_tags
__all__ = ["join_tags"]
import argparse
from . import join_tags
parser = argparse.ArgumentParser()
parser.add_argument("--sep", default=",")
parser.add_argument("tags", nargs="*")
args = parser.parse_args()
print(join_tags(args.tags, args.sep))
from tagfmt import join_tags
print(join_tags(tags=["red", "blue"], sep="|"))
legacy_job.py 用旧关键字 tags、sep 调用;命令行入口把分隔符放在第二个位置参数。两者是不同的兼容要求。先看清调用形式,再决定新接口与旧接口如何连接,不能靠把消费方一并改成新写法来获得通过。
14. 先固定兼容答案,再检查候选改动
下面的答案可以直接从接口要求手工推出,不由候选代码或参考实现生成。检查函数时从包入口导入,避免只测试实现文件而漏掉导出问题。成功命令还应没有 stderr;异常只要求类型为 TypeError,不锁定某个 Python 版本的报错句子。
| 边界 | 固定输入或命令 | 独立预期 |
|---|---|---|
| 新默认参数 | from tagfmt import format_tags; format_tags(["red", "blue"]) | 返回 "red,blue" |
| 新公开入口 | from tagfmt import format_tags; format_tags(values=["red", "blue"], separator="|") | 返回 "red|blue" |
| 旧默认参数 | join_tags(["red", "blue"]) | 返回 "red,blue" |
| 旧第二位置参数 | join_tags(["red", "blue"], "|") | 返回 "red|blue" |
| 旧关键字脚本 | python3 -B legacy_job.py | stdout 为 red|blue 后接一个换行;退出 0 |
| 旧命令行入口 | python3 -B -m tagfmt --sep "|" red blue | stdout 为 red|blue 后接一个换行;退出 0 |
| 包的公开名称 | sorted(tagfmt.__all__) | 恰为 ["format_tags", "join_tags"] |
| 新参数仍是仅关键字 | format_tags(["red", "blue"], "|") | 抛出 TypeError;不能把新接口改成接受第二位置参数 |
| 值和空输入 | values = [" red ", "", "blue", "red"];两种入口均用 "|" 连接 | 均返回 " red ||blue|red",values 不变;两个入口处理 [] 均返回 "" |
把验收分成两步:先核对修改范围,确认两个旧调用方仍逐字节不变,没有新增或删除其它文件;再运行固定命令。即使全部示例输出吻合,也要检查实现是否只为样例硬编码。这里只限定字符串列表与字符串分隔符,故意不把额外类型校验、警告或格式清理加入任务。
随附的 contract_check.py 逐条执行这些公开命令,预期 stdout 预先写在 expected_cases.json,不从参考版本采样。它不检查文件范围,也不是安全沙箱;运行候选代码仍需使用无私人文件、凭据和生产挂载的一次性隔离环境。
15. 失败示范:函数别名不会保留旧参数名
一个很容易漏审的改法,是先实现新函数,再写 join_tags = format_tags。以下是人工构造的错误版本:两个公开名称都有了,新入口也能返回正确字符串,但两个名字指向的仍是同一函数,其参数约定没有变回旧接口。
def format_tags(values, *, separator=","):
return separator.join(values)
join_tags = format_tags
from .core import format_tags, join_tags
__all__ = ["join_tags", "format_tags"]
因此,join_tags(["red", "blue"], "|") 多传了一个位置参数;legacy_job.py 的 tags=、sep= 也不是新函数接受的参数名。旧 CLI 和旧脚本都会失败。只运行新 API 的正常输入,发现不了这个交付缺口。
一种完整的人工参考改法,是保留旧签名,用薄包装把旧参数映射到新参数,包入口同时公开两者。下面替换两个允许修改的文件即可,其余输入保持原样;这是参考答案,不是任何产品的生成结果。
def format_tags(values, *, separator=","):
return separator.join(values)
def join_tags(tags, sep=","):
return format_tags(tags, separator=sep)
from .core import format_tags, join_tags
__all__ = ["join_tags", "format_tags"]
本例已在本地执行人工输入、错误版本与参考版本:原始输入保留旧入口但缺少新 API;错误别名版本触发了上述旧调用失败;参考版本符合固定命令的预期。这里确认的是样例的因果关系,不是两家 CLI 的完成率、可靠性或安全性。
参数与名称绑定可参照 Python 函数参数说明;包入口的约定可参照 Python 包导出说明。
16. 记录第一轮与恢复轮,不替产品预填成绩
真正比较时,把人工参考和错误示范留在任务目录之外,不让待测会话读取。两边拿到相同四文件、固定任务与公开验收要求;每边单独保留原始改动、外部命令输出和范围审查。Codex CLI 与 Claude Code 的产品记录在这组练习里均为 UNTESTED,没有模型、耗时、费用或胜负数据。
第一轮出现兼容失败时,先保存失败证据,再按事先约定决定是否追加反馈。以下反馈可以作为同类缺陷的第二轮输入,不给某一边临时增加实现提示。第二轮结果单列,不能覆盖第一轮记录。
第一轮已结束,请保留第一轮改动和检查记录。当前旧调用未达到验收:保留 join_tags(tags, sep=",") 的位置参数及旧关键字绑定,不要修改 legacy_job.py 或 tagfmt/__main__.py。请仅在允许的两个文件里修复兼容入口,并重新检查全部新旧入口;说明本轮相对第一轮改变了什么。
审阅时看三份可核对的交付:新 API 是否达到要求、旧消费者是否原样继续工作、允许范围内的 diff 是否说得清楚。若只能运行新入口,旧脚本和 CLI 尚未检查,就明确写“兼容性未验收”,不要替它补成通过。若环境问题导致命令根本没启动,也另记原因,不冒充代码失败。
这个任务的价值,是让一次跨文件迁移具有可审阅的完成条件。公开的小样例不能推断产品在真实仓库的表现;它只是帮助读者在自己的授权配置下,收集可比的第一轮与恢复轮证据。
下载完整输入、固定验收命令、人工失败与参考改法、待运行记录模板。下载包包含可直接复现的全部文件;其中的 README.txt 说明如何把参考资料与待测目录分开。
17. 完整离线实验源码
以下文件组成可独立运行的离线实验包。保存时保留标签中的相对路径。测试和参考实现由人工编写,模型结果栏没有填入模拟成绩。Python 源码面向 Linux 或 macOS 的 Python 3.10+ 与 Git 环境。
保存夹具时还要保留字节格式:benchmark/fixture/ 下的 .gitignore、AGENTS.md、CLAUDE.md、ranges.py 和 test_ranges.py 均使用 UTF-8 无 BOM、LF 换行,文件末尾恰好保留一个换行符。页面代码块不显示末尾换行,从 HTML 提取或复制后请在编辑器中核对;不要额外添加空行。manifest.json 的文件哈希和固定 Git commit 都依赖这些字节。若 prepare.py 报 Unexpected base commit,先比对这五个文件与 manifest 中的 SHA-256,再检查换行和 Git 配置;不要直接改 manifest 或跳过基线检查来让测试通过。修正原始夹具后,换一个尚不存在的目标目录重新准备,保留失败记录。
benchmark/prompt.txt
Only edit ranges.py. Fix merge_ranges for lists of integer [start, end] pairs.
Intervals are half-open. Merge overlapping intervals; keep touching ones apart.
Return sorted lists. Do not mutate the input. Empty input returns an empty list.
Reject start >= end with ValueError. Other input validation is out of scope.
Read repository files and run python3 -B -m unittest -v only as needed.
Do not edit tests, instruction files, or Git metadata. Do not commit changes.
Do not access the network, install packages, or use MCP, hooks, or extra agents.
When finished, report the changed file, test command, test result, and caveats.benchmark/fixture/ranges.py
def merge_ranges(ranges):
ordered = sorted(ranges)
result = [ordered[0]]
for start, end in ordered[1:]:
if start <= result[-1][1]:
result[-1][1] = max(result[-1][1], end)
else:
result.append([start, end])
return resultbenchmark/fixture/test_ranges.py
import unittest
from ranges import merge_ranges
class RangeTests(unittest.TestCase):
def test_empty(self):
self.assertEqual(merge_ranges([]), [])
def test_single(self):
self.assertEqual(merge_ranges([[1, 3]]), [[1, 3]])
def test_disjoint(self):
self.assertEqual(merge_ranges([[1, 2], [4, 5]]), [[1, 2], [4, 5]])
def test_touching(self):
self.assertEqual(merge_ranges([[1, 3], [3, 5]]), [[1, 3], [3, 5]])
def test_overlap(self):
self.assertEqual(merge_ranges([[1, 4], [2, 7]]), [[1, 7]])
def test_nested(self):
self.assertEqual(merge_ranges([[1, 8], [2, 3]]), [[1, 8]])
def test_unsorted_and_unchanged(self):
data = [[4, 8], [1, 5]]
self.assertEqual(merge_ranges(data), [[1, 8]])
self.assertEqual(data, [[4, 8], [1, 5]])
def test_zero_width(self):
with self.assertRaises(ValueError):
merge_ranges([[2, 2]])
def test_reversed(self):
with self.assertRaises(ValueError):
merge_ranges([[5, 2]])
if __name__ == '__main__':
unittest.main()benchmark/fixture/AGENTS.md
Only edit ranges.py. Fix merge_ranges for lists of integer [start, end] pairs.
Intervals are half-open. Merge overlapping intervals; keep touching ones apart.
Return sorted lists. Do not mutate the input. Empty input returns an empty list.
Reject start >= end with ValueError. Other input validation is out of scope.
Read repository files and run python3 -B -m unittest -v only as needed.
Do not edit tests, instruction files, or Git metadata. Do not commit changes.
Do not access the network, install packages, or use MCP, hooks, or extra agents.
When finished, report the changed file, test command, test result, and caveats.benchmark/fixture/CLAUDE.md
Only edit ranges.py. Fix merge_ranges for lists of integer [start, end] pairs.
Intervals are half-open. Merge overlapping intervals; keep touching ones apart.
Return sorted lists. Do not mutate the input. Empty input returns an empty list.
Reject start >= end with ValueError. Other input validation is out of scope.
Read repository files and run python3 -B -m unittest -v only as needed.
Do not edit tests, instruction files, or Git metadata. Do not commit changes.
Do not access the network, install packages, or use MCP, hooks, or extra agents.
When finished, report the changed file, test command, test result, and caveats.benchmark/fixture/.gitignore
__pycache__/
*.pycbenchmark/manifest.json
{
"fixture_id": "half-open-ranges-v1",
"allowed_changes": [
"ranges.py"
],
"files": {
".gitignore": "862263fa1f46c20f0d1e4dac5ffcc75abd55c08211b2c3864c5f8764b9d87793",
"AGENTS.md": "00467be35526c58119fc16406f5882c86f229da5b46a806de1f9d183cbc32abe",
"CLAUDE.md": "00467be35526c58119fc16406f5882c86f229da5b46a806de1f9d183cbc32abe",
"ranges.py": "c3e94454315f3d945ca338cc3cb7928fdb5a24a77413f557dd416f3a5c9291ab",
"test_ranges.py": "8f29517d9483d9317df31b7858d4f41c64953cc26bd81af9e1c9e8aeb64d4bac"
},
"base_commit": "abf98a76e2fd74e99c1921bffb4ee0bf776bd63f"
}benchmark/prepare.py
"""Create a new isolated fixture directory; never overwrite an existing one."""
from pathlib import Path
import hashlib
import json
import os
import shutil
import subprocess
import sys
ROOT = Path(__file__).resolve().parent
def git(directory, *args):
env = dict(os.environ, GIT_CONFIG_NOSYSTEM='1',
GIT_CONFIG_GLOBAL=os.devnull,
GIT_AUTHOR_NAME='Fixture', GIT_AUTHOR_EMAIL='fixture@example.test',
GIT_COMMITTER_NAME='Fixture',
GIT_COMMITTER_EMAIL='fixture@example.test',
GIT_AUTHOR_DATE='2026-10-05T00:00:00+0000',
GIT_COMMITTER_DATE='2026-10-05T00:00:00+0000')
for name in list(env):
if name.startswith('GIT_CONFIG_') and name not in {
'GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL'
}:
del env[name]
for name in ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE']:
env.pop(name, None)
command = ['git', '-c', 'core.hooksPath=' + os.devnull,
'-c', 'commit.gpgsign=false', '-c', 'core.autocrlf=false',
'-C', str(directory), *args]
return subprocess.check_output(command, env=env, stderr=subprocess.PIPE)
def prepare(destination):
destination = Path(destination).resolve()
shutil.copytree(ROOT / 'fixture', destination)
git(destination, 'init', '-q', '--initial-branch=main', '--object-format=sha1')
git(destination, 'add', '.')
git(destination, 'commit', '-qm', 'fixed comparison fixture v1')
commit = git(destination, 'rev-parse', 'HEAD').decode().strip()
expected = json.loads((ROOT / 'manifest.json').read_text())
if expected.get('base_commit') and commit != expected['base_commit']:
raise RuntimeError('Unexpected base commit; inspect Git configuration')
return {'path': str(destination), 'base_commit': commit}
if __name__ == '__main__':
print(json.dumps(prepare(sys.argv[1]), indent=2))benchmark/trusted_runner.py
"""Run a trusted copy of the tests against one candidate module.
This executes candidate Python and is NOT a sandbox. Use a disposable isolated
machine/container without private files, credentials, or production mounts.
"""
from pathlib import Path
import importlib.util
import json
import sys
import unittest
root = Path(__file__).resolve().parent
candidate = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location('ranges', candidate)
module = importlib.util.module_from_spec(spec)
sys.modules['ranges'] = module
spec.loader.exec_module(module)
tests = importlib.util.spec_from_file_location('trusted_tests',
root / 'fixture/test_ranges.py')
test_module = importlib.util.module_from_spec(tests)
tests.loader.exec_module(test_module)
suite = unittest.defaultTestLoader.loadTestsFromModule(test_module)
result = unittest.TextTestRunner(stream=sys.stderr, verbosity=2).run(suite)
print(json.dumps({'tests_run': result.testsRun, 'failures': len(result.failures),
'errors': len(result.errors), 'passed': result.wasSuccessful()}))
sys.exit(0 if result.wasSuccessful() else 1)benchmark/evaluate.py
"""Offline functional/scope evaluation, with no calls to either model.
A passing result is limited to these public tests and file checks. It cannot
prove security isolation or compliance with runtime/network restrictions.
Run only reviewed candidate code in a disposable isolated environment.
"""
from pathlib import Path
import argparse
import difflib
import hashlib
import json
import os
import signal
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent
def snapshot(directory):
files = {}
for path in sorted(directory.rglob('*')):
relative = path.relative_to(directory)
if '.git' in relative.parts or '__pycache__' in relative.parts:
continue
if path.is_symlink():
files[str(relative)] = 'SYMLINK'
elif path.is_file():
files[str(relative)] = hashlib.sha256(path.read_bytes()).hexdigest()
return files
def run_tests(module, timeout):
command = [sys.executable, '-I', '-B', str(ROOT / 'trusted_runner.py'),
str(module)]
# Only POSIX is supported so the entire process group can be stopped.
with tempfile.TemporaryDirectory() as working:
with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
process = subprocess.Popen(command, cwd=working, stdout=out,
stderr=err, start_new_session=True)
timed_out = False
try:
process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
timed_out = True
finally:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
process.wait()
out.seek(0)
err.seek(0)
stdout = out.read(100000).decode(errors='replace')
stderr = err.read(100000).decode(errors='replace')
summary = None
if not timed_out:
try:
summary = json.loads(stdout)
except (json.JSONDecodeError, TypeError):
pass
schema_ok = (isinstance(summary, dict)
and all(type(summary.get(k)) is int
for k in ['tests_run', 'failures', 'errors'])
and type(summary.get('passed')) is bool)
passed = (process.returncode == 0 and schema_ok
and summary['tests_run'] == 9 and summary['passed']
and summary['failures'] == 0 and summary['errors'] == 0)
return {'exit_code': process.returncode, 'timed_out': timed_out,
'summary': summary, 'passed': passed, 'stdout': stdout,
'stderr': stderr}
def evaluate(directory, timeout=10):
directory = Path(directory).resolve()
manifest = json.loads((ROOT / 'manifest.json').read_text())
current = snapshot(directory)
baseline = manifest['files']
changed = sorted(k for k in set(current) | set(baseline)
if current.get(k) != baseline.get(k))
unexpected = sorted(set(changed) - set(manifest['allowed_changes']))
module = directory / 'ranges.py'
module_ok = module.is_file() and not module.is_symlink()
git_head = None
try:
head = (directory / '.git/HEAD').read_text().strip()
if head == 'ref: refs/heads/main':
git_head = (directory / '.git/refs/heads/main').read_text().strip()
except (OSError, UnicodeError):
pass
base_ok = git_head == manifest['base_commit']
scope_ok = not unexpected and module_ok and base_ok
result = run_tests(module, timeout) if scope_ok else None
diff = ''
if module_ok:
diff = ''.join(difflib.unified_diff(
(ROOT / 'fixture/ranges.py').read_text().splitlines(keepends=True),
module.read_text(errors='replace').splitlines(keepends=True),
fromfile='baseline/ranges.py', tofile='candidate/ranges.py'))
additions = sum(x.startswith('+') and not x.startswith('+++')
for x in diff.splitlines())
removals = sum(x.startswith('-') and not x.startswith('---')
for x in diff.splitlines())
return {'fixture_id': manifest['fixture_id'],
'expected_base_commit': manifest['base_commit'],
'base_head_unchanged': base_ok, 'changed_files': changed,
'unexpected_changes': unexpected, 'scope_passed': scope_ok,
'tests': result, 'diff_added': additions, 'diff_removed': removals,
'diff': diff,
'accepted': scope_ok and bool(result and result['passed']),
'runtime_restrictions_verified': False,
'model_comparison_performed': False}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('candidate')
parser.add_argument('--out', required=True)
args = parser.parse_args()
if os.name != 'posix':
parser.error('This evaluator requires Linux or macOS (POSIX)')
report = evaluate(args.candidate)
Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=2))
print('accepted=' + str(report['accepted']))
sys.exit(0 if report['accepted'] else 1)benchmark/test_evaluator.py
"""All cases are human-authored offline checks, never product measurements."""
from pathlib import Path
import json
import tempfile
import unittest
from evaluate import evaluate
from prepare import prepare
ROOT = Path(__file__).resolve().parent
class EvaluatorTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.case = Path(self.temp.name) / 'case'
prepare(self.case)
def tearDown(self):
self.temp.cleanup()
def repair(self):
(self.case / 'ranges.py').write_bytes(
(ROOT / 'manual_reference.py').read_bytes())
def test_buggy_fixture_rejected(self):
result = evaluate(self.case)
self.assertFalse(result['accepted'])
self.assertEqual(result['tests']['summary']['tests_run'], 9)
self.assertEqual(result['tests']['summary']['failures'], 4)
self.assertEqual(result['tests']['summary']['errors'], 1)
def test_manual_reference_passes(self):
self.repair()
result = evaluate(self.case)
self.assertTrue(result['accepted'])
self.assertEqual(result['changed_files'], ['ranges.py'])
self.assertFalse(result['model_comparison_performed'])
def test_same_commit_for_independent_fixtures(self):
other = prepare(Path(self.temp.name) / 'other')
expected = json.loads((ROOT / 'manifest.json').read_text())
self.assertEqual(other['base_commit'], expected['base_commit'])
def test_modified_tests_rejected(self):
self.repair()
(self.case / 'test_ranges.py').write_text('')
result = evaluate(self.case)
self.assertFalse(result['accepted'])
self.assertIsNone(result['tests'])
def test_deleted_instruction_rejected(self):
self.repair()
(self.case / 'AGENTS.md').unlink()
self.assertFalse(evaluate(self.case)['accepted'])
def test_added_file_rejected(self):
self.repair()
(self.case / 'extra.py').write_text('# extra\n')
self.assertFalse(evaluate(self.case)['accepted'])
def test_symlink_rejected_without_execution(self):
(self.case / 'ranges.py').unlink()
(self.case / 'ranges.py').symlink_to(ROOT / 'manual_reference.py')
result = evaluate(self.case)
self.assertFalse(result['accepted'])
self.assertIsNone(result['tests'])
def test_changed_head_rejected(self):
self.repair()
(self.case / '.git/refs/heads/main').write_text('0' * 40 + '\n')
self.assertFalse(evaluate(self.case)['accepted'])
def test_summary_schema_rejected(self):
summaries = ['[]', 'null', '1', 'true', '{}',
'{"tests_run":true,"failures":0,"errors":0,"passed":true}',
'{"tests_run":9,"failures":false,"errors":0,"passed":true}',
'{"tests_run":9,"failures":0,"errors":0,"passed":1}']
for summary in summaries:
with self.subTest(summary=summary):
program = 'import sys; print(' + repr(summary) + '); sys.exit(0)'
(self.case / 'ranges.py').write_text(program)
self.assertFalse(evaluate(self.case)['accepted'])
def test_timeout_rejected(self):
(self.case / 'ranges.py').write_text('while True:\n pass\n')
result = evaluate(self.case, timeout=0.3)
self.assertFalse(result['accepted'])
self.assertTrue(result['tests']['timed_out'])
if __name__ == '__main__':
unittest.main()benchmark/manual_reference.py
# Human-authored reference for offline evaluator checks, not a product result.
def merge_ranges(ranges):
result = []
for start, end in sorted(ranges):
if start >= end:
raise ValueError('start must be smaller than end')
if result and start < result[-1][1]:
result[-1][1] = max(result[-1][1], end)
else:
result.append([start, end])
return resultbenchmark/run-record.template.json
{
"tool": null,
"cli_version": null,
"model_id": null,
"effort": null,
"permission_configuration": null,
"sandbox_configuration": null,
"loaded_instructions": null,
"enabled_extensions": null,
"os_python_git": null,
"base_commit": null,
"prompt_sha256": null,
"session_id": null,
"start_utc": null,
"end_utc": null,
"wall_seconds": null,
"approval_wait_seconds": null,
"intervention_events": [],
"cli_exit_code": null,
"final_message": null,
"evaluation_report": null,
"reported_usage": null,
"actual_billed_cost": null,
"run_status": "NOT_RUN",
"origin": "reader_real_run_template"
}benchmark/results.template.csv
tool,run,model,version,tests,scope,wall_seconds,interventions,status
Codex,1,,,,,,,NOT_RUN
Claude Code,1,,,,,,,NOT_RUNbenchmark/README.md
# Codex / Claude Code comparison fixture
This package measures no product by itself. No paid or real model call was run.
The source fixture and evaluator need only Python 3.10+ and Git, on Linux/macOS.
Run candidate code only in a disposable isolated environment without secrets.
The evaluator is not a security sandbox and does not audit all Git metadata.
## Prepare two clean task repositories
From the directory containing this benchmark folder:
python3 benchmark/prepare.py trial-codex
python3 benchmark/prepare.py trial-claude
Both repositories have the same fixed base commit. The manifest contains hashes
for every task file. AGENTS.md and CLAUDE.md contain identical task instructions.
No model launcher is included: choose and record an authorized model and CLI.
Start a fresh interactive session in each trial and paste benchmark/prompt.txt.
The prompt allows editing only ranges.py, reading task files, and running:
python3 -B -m unittest -v
Use the same approved environment constraints for both tools. Disable unrelated
extensions and memories in a dedicated test profile, without weakening managed
security policy. Model access needs provider connectivity; the task itself
needs no network, package downloads, MCP, hooks, or extra agents. Log all
approval waits, denials, hints, retries, and manual edits outside the trial.
Stop each run when its final answer arrives or the predeclared time limit ends.
Do not give a second prompt until the first-attempt result has been recorded.
## Evaluate after the agent has stopped
Inspect its diff first. In the disposable test environment, from the parent:
python3 benchmark/evaluate.py trial-codex --out codex-eval.json
python3 benchmark/evaluate.py trial-claude --out claude-eval.json
Exit code 0 means the nine public behavior tests and checked scope passed.
Exit code 1 means failure, timeout, or a scope violation. JSON retains details.
Do not confuse these exits with either product's own exit status.
Do not modify this benchmark directory or its manifest between candidates.
File checks run before candidate execution. They do not detect writes made
during candidate execution. They ignore .git and __pycache__ contents;
only HEAD's fixed branch ref
is checked. Transient writes and prohibited network operations need external
sandbox/audit evidence. Tests can be gamed; keep manual code review.
Record versions, exact model, permission snapshot, prompt hash, session ID,
interventions and real timings in run-record.template.json. Blank fields and
NOT_RUN mean no measurement, never zero. No model speed/quality/cost ranking
can be derived from the included offline self-tests.
## Offline evaluator verification only
cd benchmark
python3 -B -m unittest -v test_evaluator
Ten harness tests include the intentionally broken fixture (4 failures and
1 error among 9 behavior tests), a human-written reference (9 pass), modified
tests/instructions, an extra file, a symlink, a changed Git ref, timeout,
and malformed summary JSON.
manual_reference.py is a human fixture for harness testing. Never show it to
agents under evaluation. It is not a Codex or Claude Code output.
## Limits
This small task checks boundary handling and workflow reproducibility. It says
nothing about whole-repository reasoning, UI implementation, large migrations,
security performance, or either model's general competence. Repeated real runs
and representative tasks are required for a useful personal evaluation.benchmark/hook_contract_check.py · 仅离线协议示意
#!/usr/bin/env python3
"""Offline teaching fixture; NOT a Codex/Claude hook or host implementation.
Only fixed PreToolUse cases below are covered. No CLI, model, network, shell,
user hook files, or existing repositories are used. Failure/timeout are inputs,
not real process experiments. Unsupported scope returns unresolved.
Documentation checked 2026-10-05:
https://learn.chatgpt.com/docs/hooks
https://code.claude.com/docs/en/hooks
"""
import json
import re
import tempfile
from pathlib import Path
def decision(value):
return json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": value,
"permissionDecisionReason": "Harmless migration fixture.",
}})
def classify(profile, raw, *, exit_code=0, timed_out=False, matcher="^Bash$"):
"""Return (hook_state, route) for this fixture's cases, not all valid JSON.
normal means ordinary permission checks remain, NOT approved execution.
stop and ask remain distinct from deny. Unknown Codex failure branches
deliberately stay unresolved. No mixed-exit/JSON precedence is simulated.
"""
if profile not in {"codex-doc", "claude-command-doc"}:
raise ValueError("Unknown documentation profile")
if not re.search(matcher, "Bash"):
return "not_invoked", "normal"
is_claude = profile == "claude-command-doc"
if timed_out:
return "timeout", "normal" if is_claude else "unresolved"
if exit_code == 2 and not raw:
return "exit_2", "deny"
if exit_code != 0:
if exit_code == 1 and not raw:
return "error", "normal" if is_claude else "unresolved"
return "outside_fixture", "unresolved"
if not raw:
return "no_decision", "normal"
try:
value = json.loads(raw)
except json.JSONDecodeError:
return "invalid_json", "normal" if is_claude else "unresolved"
if not isinstance(value, dict):
return "outside_fixture", "unresolved"
stop = {"continue": False, "stopReason": "Fixture stop."}
if value == stop:
return ("accepted", "stop") if is_claude else ("unsupported", "normal")
if value == {"decision": "block", "reason": "Fixture block."}:
return "legacy_block", "deny"
for name in ("deny", "ask"):
if value == json.loads(decision(name)):
if name == "ask" and not is_claude:
return "unsupported", "normal"
return "accepted", name
return "outside_fixture", "unresolved"
def marker_probe(directory, key, route, ordinary_permission):
"""A local stand-in, not a host call. None means no outcome was modeled."""
marker = directory / (key + ".marker")
assert not marker.exists()
if route == "unresolved":
return None
if route == "normal" and ordinary_permission:
marker.write_text("offline fixture only\n", encoding="utf-8")
return marker.exists()
def check():
stop = json.dumps({"continue": False, "stopReason": "Fixture stop."})
legacy = json.dumps({"decision": "block", "reason": "Fixture block."})
cases = [
("positive", "", {}, ("no_decision", "normal"),
("no_decision", "normal")),
("deny", decision("deny"), {}, ("accepted", "deny"),
("accepted", "deny")),
("migrated_stop", stop, {}, ("unsupported", "normal"),
("accepted", "stop")),
("migrated_ask", decision("ask"), {}, ("unsupported", "normal"),
("accepted", "ask")),
("legacy_block", legacy, {}, ("legacy_block", "deny"),
("legacy_block", "deny")),
("exit_2", "", {"exit_code": 2}, ("exit_2", "deny"),
("exit_2", "deny")),
("error", "", {"exit_code": 1}, ("error", "unresolved"),
("error", "normal")),
("bad_json", "{broken}", {}, ("invalid_json", "unresolved"),
("invalid_json", "normal")),
("timeout", "", {"timed_out": True}, ("timeout", "unresolved"),
("timeout", "normal")),
("matcher_miss", decision("deny"), {"matcher": "^Read$"},
("not_invoked", "normal"), ("not_invoked", "normal")),
("outside_fixture", '{"other":true}', {},
("outside_fixture", "unresolved"), ("outside_fixture", "unresolved")),
]
rows = []
profiles = ("codex-doc", "claude-command-doc")
with tempfile.TemporaryDirectory(prefix="hook-contract-") as tmp:
directory = Path(tmp)
for name, raw, options, codex_expected, claude_expected in cases:
for profile, expected in zip(profiles, (codex_expected, claude_expected)):
state, route = classify(profile, raw, **options)
assert (state, route) == expected, (name, profile, state, route)
# Check both independent permission outcomes. A missing marker
# by itself therefore cannot identify the rejecting layer.
for permission in (True, False):
key = f"{name}-{profile}-{permission}"
marker = marker_probe(directory, key, route, permission)
want = None if route == "unresolved" else (
route == "normal" and permission
)
assert marker is want, (key, marker, want)
rows.append({
"case": name,
"profile": profile,
"hook_state": state,
"route": route,
"ordinary_permission": permission,
"marker_exists": marker,
"evidence": "offline_simulation",
})
return {
"evidence": "offline_simulation",
"real_host_runs": 0,
"real_model_calls": 0,
"scenario_count": len(cases),
"profile_scenarios": len(cases) * len(profiles),
"marker_checks": len(rows),
"ask_scope": "Prompt requested; no answer or noninteractive host modeled.",
"rows": rows,
}
if __name__ == "__main__":
print(json.dumps(check(), ensure_ascii=False, indent=2))
18. 核对来源与适用日期
以下均为官方文档,核对于 2026 年 10 月 5 日。文档反映核对时的公开能力,已安装版本和组织配置可能不同。命令帮助证据限本机 Codex CLI 0.159.2;Claude Code 本机未安装。
这篇能说明哪个模型写代码更好吗?
不能。本文核对公开工作流并验证离线实验夹具,没有运行真实模型比较。模型质量需要指定版本、任务集和重复运行结果。
Claude Code 能读取 AGENTS.md 吗?
当前官方文档支持,直接读取要求 v2.1.277+,且受 CLAUDE.md 的存在、设置和插件状态等条件影响。请核对当前版本实际加载的指令。
十项离线自测通过,意味着产品任务完成了吗?
只说明评估器对已知输入能识别成功和失败。两个产品的真实结果仍为待运行;人工参考实现不计入产品成绩。
能在有生产凭据的日常电脑上直接跑评估器吗?
不建议。评估器会执行候选 Python,并非安全沙箱。应使用没有私人文件、凭据和生产挂载的一次性隔离环境。
怎样比较价格与速度?
为同一类任务记录实际模型、重复次数、墙钟时间、批准等待和账单数据。先确认测试与范围合格,缺失费用留空;本文没有费用或速度排名。