get-shit-done 修复 3346 深度解析:Codex AoT Hooks 迁移中 TOML 叶子键必须取事件名而非位置元组
2026/9/7 17:53:42 网站建设 项目流程

get-shit-done 修复 #3346 深度解析:Codex AoT Hooks 迁移中 TOML 叶子键必须取事件名而非位置元组

【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done

本文基于 get-shit-done 仓库中的 changeset 记录 .changeset/3346-codex-aot-toml-key.md 展开,完整还原 #3346 这个 bug 的故障现场:在 Windows 上为老版本 Codex 配置做 hooks 格式迁移时,migrateCodexHooksMapFormat把位置元组原样当作了 TOML 叶子键,导致 Codex 0.124.0+ 拒绝加载配置、安装中断。结合 bin/install.js 中的迁移实现、tests/bug-3346-codex-aot-toml-key.test.cjs 回归测试以及后置 schema 校验链路,本文会讲清楚修复的判定规则(正文event = "..."优先作为叶子键)、迁移后目标配置的两级嵌套结构,以及“迁移 → 校验 → 原子写入 → 失败回滚”这一整条安装管线的安全设计,帮助维护多运行时(Claude Code / Codex 等)配置迁移逻辑的开发者理解如何安全地做 TOML 格式升级。

1. 问题背景:Codex hooks 配置从 map 格式迁移到 AoT 格式

changeset 记录的核心事实是:Codex 0.124.0 把 hooks 配置从旧的 map 风格([hooks.<X>]表格键 + 处理器字段)改成了新的 array-of-tables(AoT)格式。这一点可以从 bin/install.js 中migrateCodexHooksMapFormat函数的头注直接读到:

Codex 0.124.0 changed from the old map-style hooks config: [hooks] [hooks.shell] command = "..." to the new array-of-tables format.

对于 get-shit-done 这样的多运行时安装器,安装/更新时(npx get-shit-done-cc@latest)会在 Codex 目标目录下读取config.toml,把用户机器上遗留的旧格式 hooks 段自动迁移到新版结构,否则新版 Codex CLI 直接拒绝加载整个配置文件。changeset 中提到的用户可见症状即:在“早于 AoT 迁移时期”的 Windows 配置上,这条迁移路径会产出非法 TOML,最终导致 Codex 运行时安装中止。

而 #3346 要回答的更细一层的问题是:迁移时新的[[hooks.<EVENT>]]头部里,<EVENT>这一段(叶子键)到底应该取什么?

2. 故障现场:位置元组被原样当作叶子键

老的 Codex 版本在写[hooks.<X>]段时,表格键<X>并不总是事件名,有时是一个<file>:<event>:<line>:<col>形式的位置标识符(diagnostic location identifier),真正的 eventName 放在段正文的event = "..."字段里。changeset 给出的真实故障样本是:

[hooks."C:\Users\helen\.codex\config.toml:session_start:0:0"] event = "session_start" command = "echo hi"

修复前的迁移逻辑直接把[hooks.<X>]的路径段<X>原样(verbatim)作为新 AoT 块的叶子键重新输出,于是生成了如下头部:

[[hooks."C:\Users\helen\.codex\config.toml:session_start:0:0"]]

这个键链对 Codex 0.124.0+ 而言不是合法的事件名叶子键,Codex 会拒绝加载该配置——“叶子键段应该是事件名,而不是诊断位置标识符”(tests/bug-3346-codex-aot-toml-key.test.cjs 头注中的原话)。由于 Windows 用户的老配置更容易出现这种带完整文件路径的表格键(C:\Users\...),该问题在 Windows 上集中爆发。

3. 迁移器实现:三类遗留形态的统一识别

migrateCodexHooksMapFormat 的职责是检测配置中所有不符合新版形态的 hooks 段并转换。从源码结构看,它通过getTomlTableSections把整个 TOML 切成表格段,然后识别三类需要迁移的遗留形态:

  1. map 格式段(bin/install.js#L3673-L3679):裸[hooks]容器段,以及恰好两段路径、非数组的[hooks.<TYPE>]事件表格。这里有两个关键排除规则:

    • section.segments.length === 2(真实解析出的键段数)而不是对section.pathstartsWith或按.分割判断,避免把[hooks.SessionStart.hooks]这类三段嵌套处理器表误识别为名为SessionStart.hooks的事件,也避免把带引号且含点号的键(如[[hooks."before.tool"]])误判;
    • 排除hooks.statehooks.state.*——这是 Codex CLI 0.130.0+ 的持久化 hook 信任命名空间,永远使用普通表格形态,绝不使用 AoT。
  2. 扁平 AoT 段(bin/install.js#L3686-L3688):path === 'hooks'且为数组的[[hooks]]条目。扁平[[hooks]]与命名空间[[hooks.<EVENT>]]不能在同一文件共存(hooks不可能同时是数组又是表),因此每个扁平条目要按其event键迁移为[[hooks.<EVENT>]]

  3. 过期命名空间 AoT 段(bin/install.js#L3696-L3712):[[hooks.<TYPE>]]条目在事件条目层级直接携带处理器字段(commandtypetimeoutstatusMessage,见STALE_HANDLER_FIELD_PATTERN),但没有嵌套的[[hooks.<TYPE>.hooks]]子表——这是 #2773 之前、Codex 0.124.0+ 拒绝的单块形态,需要提升为两级嵌套形态。已经带.hooks子表的条目不动;只有 matcher 字段、没有处理器字段的条目是合法形态,有意跳过。

三类段都为空时,函数直接原样返回内容,不做任何改动。

4. 修复核心:正文event = "..."优先决定叶子键

#3346 的修复集中在 map 格式分支的重新输出逻辑(bin/install.js#L3811-L3824),源码注释直接标注了#3346

// #3346: when the legacy `[hooks.<X>]` body declares `event = "..."`, // prefer that as the event-name leaf key. The path segment <X> may be // a `<file>:<event>:<line>:<col>` location identifier (Codex pre-AoT // wrote those as table keys), which is not a valid leaf event name — // emitting it verbatim produces a TOML key chain Codex 0.124.0+ rejects. const bodyEvent = extractFlatHookEventName(body); const type = bodyEvent !== null ? bodyEvent : s.path.slice('hooks.'.length); const skipKeys = bodyEvent !== null ? new Set(['event']) : new Set(); return buildNestedBlock(type, body, skipKeys);

判定规则可以归纳为一条:如果段正文声明了event = "...",该事件名胜出,作为叶子键;否则回退到原路径段。并且当事件名来自正文时,event字段被加入skipKeys,从重新输出的处理器正文中剔除——事件名已经被“提升”到了头部键里,处理器体里再保留一份event属于冗余遗留字段。

changeset 的结论句与此一致:“map 格式分支与过期命名空间 AoT 分支现在镜像了扁平 AoT 分支”的做法。过期 AoT 分支的对应位置在 bin/install.js#L3831-L3836,同样是bodyEvent !== null ? bodyEvent : 路径段的取值。

事件名的提取由 extractFlatHookEventName 完成,其严谨性体现在:

  • 同时接受 TOML 双引号(含转义)与单引号字符串;
  • 显式拒绝空事件名(event = ""event = '')——无法做有意义的命名空间化,条目保持原样不动。

叶子键在写入头部前还要经过 tomlBareKey 的引号处理:bare key 只允许[A-Za-z0-9_-],含空格、点号等字符的事件名(例如Before Tool)必须包成双引号 TOML 字符串并对反斜杠与双引号做转义,否则会产生非法 TOML。

5. 迁移目标形态:两级嵌套 AoT 与处理器字段归属

buildNestedBlock(bin/install.js#L3763-L3776)负责生成迁移后的目标结构。它对段正文按字段分层:只有matcher属于事件层级,其余字段都属于处理器层级parseHooksBody,bin/install.js#L3726-L3751);字段解析使用parseTomlKey而非旧的/^([\w.]+)\s*=/正则,使连字符键(如status-message)与引号键也能被正确识别——旧正则会静默丢弃这些键。

以故障样本为例,修复后迁移输出为:

[[hooks.session_start]] [[hooks.session_start.hooks]] type = "command" command = "echo hi"

几个值得注意的实现细节:

  • 若处理器字段为空(例如仅含matcher的过滤条目),不会合成一个空的[[hooks.<TYPE>.hooks]]块——那样是结构合法但语义损坏的输出(没有command的处理器条目);
  • 处理器条目缺少显式type时,默认补上type = "command"hasExplicitType机制),但不会重复添加已有的type字段;
  • 插入位置有讲究:map 格式块与过期命名空间块插入在“第一个剩余表格段之前”,以保留其在文件中的相对位置;而扁平 AoT 块只能追加到文件末尾,因为 AoT 在 TOML 中不可能先于普通表格出现,插到普通表格之前会破坏[features]/[model]等的相对顺序(bin/install.js#L3805-L3810)。

6. 回归测试:三个场景锁死修复边界

tests/bug-3346-codex-aot-toml-key.test.cjs 用三个用例完整覆盖了修复的边界。值得注意的是其测试纪律(文件头注明确要求):用项目自己的parseTomlToObject解析迁移后的 TOML,对解析出的对象形状做断言,而不是 grep 原始字符串——这避免了“文本看起来对但结构不对”的假阳性。

用例 1:位置元组键 +event字段(#3346 核心断言)

const legacy = [ '[hooks."C:\\\\Users\\\\helen\\\\.codex\\\\config.toml:session_start:0:0"]', 'event = "session_start"', 'command = "echo hi"', '', ].join('\n'); const migrated = migrateCodexHooksMapFormat(legacy); const parsed = parseTomlToObject(migrated); // 核心断言:hooks 必须只以事件名为键 assert.deepEqual(Object.keys(parsed.hooks), ['session_start']);

并对两级嵌套结构逐项验证:hooks.session_start[0].hooks[0].command === 'echo hi'type默认补齐为"command",以及handlers[0].event === undefined——处理器体不得保留遗留的event字段

用例 2:显式type+ 位置元组键——验证显式type = "command"不会被迁移器重复输出,且叶子键仍取自正文event(此处事件为tool_call_pre)。

用例 3:回归护栏——标准遗留形态[hooks.session_start](表格键即事件名、正文无event字段)在修复后必须继续按原路径段迁移,确保修复没有破坏原本就正确的分支。

7. 纵深防御:迁移失败如何被校验与回滚机制兜住

单看 #3346,问题出在“迁移器输出了非法叶子键”。但从源码结构看,get-shit-done 在 Codex 安装管线中为此类错误布置了多层防线,理解这些层能说明为什么这类 bug 的影响面被限制在“安装中止 + 配置回滚”,而不是“用户拿到一个坏掉的 Codex”。

(1)迁移时机:先剥离 GSD 托管块,再迁移用户块。安装流程在 bin/install.js#L9035-L9056 中先执行stripStaleGsdHookBlocks(按 Shape 1/2/3/4 顺序剥离历史遗留的 GSD 托管 hook 块),然后才调用migrateCodexHooksMapFormat,确保迁移只触碰用户自己写的 hooks——顺序颠倒会让 GSD 自身的过期块先被迁移,导致后续剥离正则失配(#2698 的历史教训)。

(2)写前 schema 校验。即将写入的字节会先交给 validateCodexConfigSchema 解析验证。从源码结构看,它明确拒绝的形态包括:

  • 扁平[[hooks]]数组表(Codex 0.124.0+ 要求[[hooks.<Event>]]命名空间形态);
  • [hooks.<Event>]单括号表(事件处理路径必须是 AoT);
  • [[agents]]序列形态与裸[agents]表(Codex 要求[agents.<name>]结构形态);
  • hooks.state.*使用 AoT 形态(该命名空间必须是普通表格);
  • 事件条目层级出现游离的处理器字段(command/type/timeout/statusMessage)而无.hooks子表——注释明确说:迁移器应当先转换掉这些形态,“如果还出现在这里,说明迁移没覆盖到,宁可靠校验失败也不放行坏配置”(bin/install.js#L4385-L4405);
  • 处理器type只能为"command"

(3)校验/写入失败即回滚并中止。校验不通过时恢复安装前快照并抛出致命错误(bin/install.js#L9067-L9083);写入采用“写同级临时文件再renameSync覆盖”的原子方式,中途失败不会截断现有配置(bin/install.js#L9085-L9102);pre-write阶段失败(包括迁移函数本身抛错)同样被 #2760 CR5 提升为致命错误并触发快照恢复,避免降级为 warn 后继续打印 “Done!”。#3346 修复前,Windows 老配置触发的正是这条“生成非法键 → 校验/加载失败 → 安装中止”的路径;修复后该路径不再被触发。

(4)与用户既有形态的协同。hasUserNamespacedAotHooks(bin/install.js#L3897-L3899)检测用户是否已在使用[[hooks.<EVENT>]]形态;若是,GSD 托管的 hook 块必须输出同样的形状,避免扁平与命名空间两种 AoT 混用导致 round-trip 写器产生 Codex 拒绝的配置(#2760, defect 3)。

8. 发布记录与实战影响

该修复随 v1.42.1 发布。docs/RELEASE-v1.42.1.md 的 Fixed 一节将其归纳为“Codex install and hook migration are safer— AoT hooks use event-name leaf keys, duplicate legacyhooks.jsonentries are removed, user hooks are preserved, and unsupported execute-phase worktrees are blocked”,并关联了 #3346。

对使用者的实际意义是:

  • 适用前提:你使用 Codex 运行时且config.toml中存在早于 AoT 迁移时期的旧格式[hooks.<X>]段(尤其 Windows 上键为完整文件路径的位置元组形态);
  • 修复前npx get-shit-done-cc@latest在迁移时生成[[hooks."<path>:<event>:<line>:<col>"]]这类非法头部,Codex 0.124.0+ 拒绝加载,安装中止;
  • 修复后:迁移器优先读取正文event = "..."作为事件名叶子键并剔除冗余字段,老配置被正确转换为两级嵌套 AoT,安装流程可顺利完成;
  • 排查建议:若怀疑本机 Codex 配置处于非法形态,可关注安装输出中的Migrated legacy Codex [hooks] format to two-level nested AoT提示行(bin/install.js#L9055),并确认最终配置中事件处理段均为[[hooks.<EVENT>]]+[[hooks.<EVENT>.hooks]]两级结构、hooks.state(如有)为普通表格。

9. 要点小结

  1. 叶子键取值规则[hooks.<X>]迁移为[[hooks.<EVENT>]]时,正文event = "..."优先于路径段<X>;路径段可能是<file>:<event>:<line>:<col>位置元组,绝不能作为叶子键原样输出(.changeset/3346-codex-aot-toml-key.md、bin/install.js#L3815-L3823)。
  2. 三条分支同一规则:map 格式分支、过期命名空间 AoT 分支与既有扁平 AoT 分支采用相同的“正文事件名胜出 +event字段剔除”策略,避免同一文件内形态不一致。
  3. 形态细节不能省:非 bare-key 事件名要加引号转义、无type时补type = "command"、纯 matcher 条目不合成空处理器块、hooks.state永远排除在 AoT 迁移之外。
  4. 验证方式决定可信度:回归测试用parseTomlToObject断言解析后的对象形状而非字符串匹配,三个用例分别覆盖核心修复、显式type场景与标准遗留形态的回归护栏(tests/bug-3346-codex-aot-toml-key.test.cjs)。
  5. 管线级兜底:剥离 → 迁移 → 写前 schema 校验 → 原子写入 → 失败恢复快照的中止策略,把“迁移器写坏配置”这一类错误的最终影响收敛为“本次安装中止且配置还原”,而不是交付一个 Codex 无法加载的坏文件。

【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询