【免费下载链接】gsd-core
Git. Ship. Done - Core
本文基于 gsd-core(Git. Ship. Done - Core)仓库中的变更档案 .changeset/archived/mellow-lynx-forage.md(PR 3289,类型 Fixed)展开,讲清一个真实的安装器缺陷:schema 校验器曾把 Codexconfig.toml中所有hooks.*表一律按"事件处理器的数组表(array-of-tables,下称 AoT)"来分类,导致 Codex CLI 0.130.0 起引入的hooks.state.<project>/...每钩子信任状态表被误判为非法、安装被拒。读完本篇,你将理解该误判的根因、修复在 bin/install.js 中的具体落点(迁移与校验两条链路的 carve-out)、以及 tests/codex-config-hooks.test.cjs 中用于锁定该行为的回归用例矩阵,从而能够在遇到 Codex 配置文件校验报错时自行定位是"事件表形状错误"还是"hooks.state 命名空间误伤"。
1. 变更档案说了什么
变更档案原文(.changeset/archived/mellow-lynx-forage.md)只有简短的 front matter 和一句话描述,front matter 标记type: Fixed、pr: 3289,正文要点是:
get-shit-done-cc --codexno longer rejects valid Codexhooks.statetrust-persistence entries — the schema validator was over-classifying everyhooks.*table as an event-handler array-of-tables, breaking installs against Codex CLI 0.130.0+ wherehooks.state.<project>/...stores per-hook trust state. Regular-table shape is now accepted forhooks.state.*whilehooks.<EVENT>still requires AoT.
把它拆解成三个可验证的事实断言:
- 症状:执行
--codex安装时,校验器拒绝(rejects)合法的hooks.state信任持久化条目,导致安装中断; - 根因:schema 校验器"过度分类"——把每一个
hooks.*表都当作事件处理器的 AoT 来处理; - 修复语义:
hooks.state.*命名空间接受常规表(regular table)形状,而hooks.<EVENT>(真正的钩子事件表)仍然要求AoT 形状,二者规则分轨。
下面结合源码逐条印证这三点,并补全"0.124.0 格式变更 → 0.130.0 新增命名空间 → 校验器踩坑"的完整时间线。
2. 背景:Codex config.toml 的钩子格式演进,与 gsd-core 为什么要在意它
gsd-core 安装到 Codex 运行时时会写入并维护用户的config.toml。在安装器源码 bin/install.js 中,钩子配置的格式演进有明确注释记录(bin/install.js#L5483-L5492):
- Codex 0.124.0 之前:map 风格的
[hooks]容器 +[hooks.<TYPE>]子表; - Codex 0.124.0 起:改为 AoT(array-of-tables)风格,即
[[hooks.<TYPE>]],且事件表内部的处理程序字段(command、type、timeout、statusMessage)必须嵌套在[[hooks.<TYPE>.hooks]]子表中; - Codex 0.130.0 起:新增
hooks.state命名空间用于持久化"每个钩子的信任状态"(per-hook trust state),形状是常规表,例如子键是带引号的项目/钩子路径字符串。
安装器对应地实现了三个关键纯函数,均位于 bin/install.js:
| 函数 | 位置 | 职责 |
|---|---|---|
migrateCodexHooksMapFormat | bin/install.js#L5503 | 把旧的 map 风格 / 扁平[[hooks]]/ 单层[[hooks.<TYPE>]]形状迁移到当前[[hooks.<EVENT>]]AoT 形式,保留键值对与注释 |
validateCodexConfigSchema | bin/install.js#L6158 | 安装前/后的结构校验:解析 TOML + 扫描表头,按 Codex 当前 schema 规则逐条放行或拒绝 |
stripStaleGsdHookBlocks | bin/install.js#L5386 | 用 TOML AST 结构性地剥离 GSD 自管的过期钩子块(按command值识别,而非正则) |
缺陷正发生在validateCodexConfigSchema与migrateCodexHooksMapFormat共同依赖的那条粗粒度规则上:"凡是hooks.*路径的表,都应按钩子事件表对待"。
3. 缺陷复盘:一个合法的 0.130.0+ 配置为什么会安装失败
Codex CLI 0.130.0+ 的用户config.toml中合法地存在这样的结构(键名形状取自仓库回归测试的真实 fixture,见 tests/codex-config-hooks.test.cjs#L2529-L2542):
[features] hooks = true [[hooks.SessionStart]] matcher = "" [[hooks.SessionStart.hooks]] command = "node gsd-check-update.js" [hooks.state] [hooks.state.'/home/user/.codex/hooks.json:pre_tool_use:0:0'] # Codex 写入的每钩子信任持久化字段注意最后一节:[hooks.state.'...']是单括号常规表,且子键是一个包含路径、冒号、点号的带引号 key。在修复前,validateCodexConfigSchema的表头检查对一切以hooks.开头的非数组表统一执行"事件表必须是 AoT"的拒绝逻辑,于是:
- 裸的
[hooks.state]容器 → 被当作"非法的单括号[hooks.<Event>]"拒绝; [hooks.state.'<key>']→ 同样被拒绝。
安装流程因此中断,报错信息形如bare [hooks.state] table is invalid in current Codex schema (expected [[hooks.state]] array-of-tables)——错误提示反而建议用户把信任状态表改写成 AoT,方向完全相反。这正是变更档案所说的 "over-classifying everyhooks.*table as an event-handler array-of-tables"。
还有一个容易忽视的次生风险:若不做 carve-out,migrateCodexHooksMapFormat会在迁移阶段把hooks.state.*这些常规表改写成[[hooks.state.*]]AoT 形式——不但不修复,还会把合法配置主动写成 Codex 拒绝的形状。因此修复必须同时落在"迁移"与"校验"两条链路上,源码中这两处都有对应的排除逻辑(见下节)。
4. 修复落点(一):迁移函数排除 hooks.state 命名空间
migrateCodexHooksMapFormat收集"遗留 map 风格节"的过滤器,在修复后显式排除hooks.state与其全部子键(bin/install.js#L5510-L5517):
// Exclude hooks.state and hooks.state.* — these are Codex's persistent hook-trust // namespace (Codex CLI 0.130.0+) and use regular-table shape, never AoT. const legacyMapSections = sections.filter( (section) => !section.array && ( section.path === 'hooks' || (section.path.startsWith('hooks.') && section.segments.length === 2 && section.path !== 'hooks.state' && !section.path.startsWith('hooks.state.')) ) );两处细节值得注意:
section.segments.length === 2而非单纯的前缀匹配。getTomlTableSections(bin/install.js#L5118-L5133)保留了表头真实解析出的 key 段数,其注释明确说明动机:要区分 2 段路径(如带引号的hooks."before.tool",key 本身含点号)与 3 段路径(如hooks.SessionStart.hooks),而不能对section.path做按点号 split——否则带引号、内含点号的事件名会被错误分级。hooks.state的排除同样依赖这个准确段数,保证[hooks.state.<key>](2 段:hooks+ 引号 key)被正确识别。!section.path.startsWith('hooks.state.')覆盖任意深度子键,hooks.state本身再用section.path !== 'hooks.state'精确排除,两者合起来封住整个命名空间。
效果:迁移阶段对hooks.state.*一律"不看见",只处理真正的遗留事件表;用户注释与信任状态字段原样保留。
5. 修复落点(二):校验器的分轨规则
validateCodexConfigSchema(bin/install.js#L6158)采用"表头形状检查 + 解析对象结构确认"双通道。修复后的分轨规则完整表述如下:
通道一:表头检查(bin/install.js#L6203-L6221)
// hooks.state.* is Codex's persistent hook-trust namespace (added in // Codex CLI 0.130.0). It uses regular-table shape, NOT array-of-tables. // [[hooks.state]] or [[hooks.state.<key>]] (AoT) is invalid; reject it. if (section.array && (section.path === 'hooks.state' || section.path.startsWith('hooks.state.'))) { return { ok: false, reason: `[[${section.path}]] is invalid; hooks.state namespace must use regular tables` }; } // All other hooks.* paths (event handlers like hooks.SessionStart) require // AoT shape — bare [hooks.<Event>] (single-bracket) is invalid. if (!section.array && section.path.startsWith('hooks.') && section.path !== 'hooks.state' && !section.path.startsWith('hooks.state.')) { return { ok: false, reason: `bare [${section.path}] table is invalid in current Codex schema (expected [[${section.path}]] array-of-tables)` }; }即一张按路径前缀分轨的规则表:
| 表头形状 | 路径 | 修复前 | 修复后 |
|---|---|---|---|
[[hooks.state]]/[[hooks.state.<key>]](AoT) | hooks.state* | 通过(误放行错误形状) | 拒绝:hooks.state namespace must use regular tables |
[hooks.state]/[hooks.state.<key>](常规表) | hooks.state* | 拒绝(误伤,本 bug) | 放行 |
[hooks.<Event>](单括号) | 其他hooks.* | 拒绝 | 仍拒绝:事件表必须 AoT |
[[hooks.<Event>]](AoT) | 其他hooks.* | 通过 | 仍通过 |
通道二:解析对象结构确认(bin/install.js#L6227-L6261)
仅靠表头还不够——同一个解析后形状可能来自不同表头写法(如[agents]+default = "x"与[agents.foo]解析结果无法仅凭对象区分,源码注释对此有专门说明),因此校验器对解析出的parsed.hooks对象再做结构确认:
if (event === 'state') { if (Array.isArray(value)) { return { ok: false, reason: `hooks.state must be a regular table/object, got array-of-tables` }; } if (typeof value !== 'object' || value === null) { return { ok: false, reason: `hooks.state must be a regular table/object, got ${typeof value}` }; } continue; } // 其余 hooks.<Event> 必须仍是数组(AoT) if (!Array.isArray(value)) { return { ok: false, reason: `hooks.${event} must be an array of tables, got ${typeof value}` }; }关键在event === 'state'分支:解析后hooks.state是一个普通对象(因为它是常规表而非 AoT),如果不在这里continue跳过,就会被下面"其他hooks.<Event>必须是数组"的循环误拒——这正是测试用例hooks.state object in parsed structure does not trigger non-array rejection(tests/codex-config-hooks.test.cjs#L2585)锁定的点。
此外,[[hooks.<Event>]]的 AoT 语义校验并未放松:每个事件条目要么只含matcher等事件级键,要么必须携带[[hooks.<Event>.hooks]]处理器子数组;处理器字段(command/type/timeout/statusMessage)出现在事件条目顶层即拒绝(bin/install.js#L6262-L6289)。这保证了修复是"精准分轨"而非"整体放宽"。
6. 回归测试矩阵:#3285 用例组如何锁定修复
回归用例集中在 tests/codex-config-hooks.test.cjs 的#3285标记用例组(tests/codex-config-hooks.test.cjs#L2518),标题直白:validateCodexConfigSchema: hooks.state is a regular table (not AoT)。核心断言覆盖:
| 用例 | 输入形状 | 期望 |
|---|---|---|
裸[hooks.state]表头 | 2 段常规表 | 通过 |
[hooks.state.<project-key>] | 带引号键的常规表子键(如[hooks.state.'/home/user/.codex/hooks.json:pre_tool_use:0:0']) | 通过 |
hooks.state与[[hooks.SessionStart]]AoT 共存 | 混合形状 | 两者都通过(分轨不互相干扰) |
解析结构中hooks.state为对象 | 通道二 | 不触发 "must be an array" 拒绝 |
多个hooks.state.<key>子键(多项目) | [hooks.state.'/project/a/...']、[hooks.state.'/project/b/...'] | 全部通过 |
[[hooks.state]] | AoT 根 | 拒绝,且 reason 提及hooks.state |
[[hooks.state.foo]] | AoT 子键 | 拒绝,且 reason 提及hooks.state |
另有端到端安装用例组#3285 — install succeeds when config.toml contains hooks.state entries(tests/codex-config-hooks.test.cjs#L2657):在临时安装目录写入含hooks.state信任条目的config.toml后执行安装,断言安装不抛异常——直接对应变更档案中 "breaking installs" 的用户症状。
仓库是只读的,你可以只读地查看上述文件,或按仓库自身测试入口运行对应用例来复现验证(例如以 Node 执行tests/codex-config-hooks.test.cjs,具体方式以仓库根 package.json 的 test 脚本与 TESTING-STANDARDS.md 为准)。
7. 给使用者的自查清单:你的 config.toml 现在应该长什么样
结合修复后的校验规则,Codex 0.130.0+ 环境下 GSD 安装路径能接受的钩子区段形状可以归纳为:
# 事件钩子:必须是 AoT,处理器字段必须嵌套在 .hooks 子表 [[hooks.SessionStart]] matcher = "" [[hooks.SessionStart.hooks]] command = "node gsd-check-update.js" # 信任持久化:必须是常规表(单括号),子键为带引号的路径字符串 [hooks.state] [hooks.state.'/home/user/.codex/hooks.json:pre_tool_use:0:0'] trusted = true # 字段内容以 Codex 实际写入为准,此处仅示意形状对照检查时的判断口诀:
- 看到
[[hooks.state开头 →形状错误,GSD 校验会明确报hooks.state namespace must use regular tables,需把双括号改回单括号常规表; - 看到
[hooks.SessionStart](单括号)→形状错误,报expected [[hooks.SessionStart]] array-of-tables,需改为 AoT; - 看到
hooks.<Event>条目顶层挂着command = ...而没有[[hooks.<Event>.hooks]]→ 这是 0.124.0 前被 Codex 拒绝的"单层形状",migrateCodexHooksMapFormat会在安装时将其提升为两级嵌套形式(bin/install.js#L5529-L5551); - 事件名含点号/空格时,表头 key 必须是带引号的 TOML 字符串(如
hooks."before.tool"),这也是getTomlTableSections坚持用解析段数而非字符串 split 的原因。
8. 小结
这次修复(PR 3289,回归组 #3285)的技术实质是:在hooks.*命名空间内部为hooks.state开了一个双向 carve-out——迁移链路不碰它(bin/install.js#L5510-L5517),校验链路对它单独分轨:常规表放行、AoT 与标量形状拒绝(bin/install.js#L6203-L6261)。它没有放松hooks.<EVENT>的 AoT 强制,也没有把校验退化为只看表头,而是保持了"表头形状 + 解析对象结构"双通道确认。对维护者而言,该用例组(tests/codex-config-hooks.test.cjs)是理解 gsd-core 如何处理"上游 CLI 格式演进 vs 本地 schema 校验"这一类问题的良好范本:每次 Codex 引入新命名空间,正确动作是精准分轨 + 全形状矩阵的拒绝/放行断言,而不是放宽前缀规则。
关联文件索引:变更档案 .changeset/archived/mellow-lynx-forage.md;安装器实现 bin/install.js(migrateCodexHooksMapFormat@ L5503、getTomlTableSections@ L5118、stripStaleGsdHookBlocks@ L5386、validateCodexConfigSchema@ L6158、installCodexConfig@ L7074);回归测试 tests/codex-config-hooks.test.cjs。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
get-shit-done(GSD)Codex Skill 物化契约解析:修复 1.42.2 回归,让 Codex CLI 0.130.0+ 重新路由 `$gsd-*` 命令
get shit done(GSD)Codex Skill 物化契约解析:修复 1.42.2 回归,让 Codex CLI 0.130.0+ 重新路由 $gsd
人工智能AI 应用提示工程开发工具工作流自动化AI Agent修复 get-shit-done-cc --codex 拒绝合法 TOML 浮点数:Codex 配置合并与安装回滚机制的源码级剖析
修复 get shit done cc codex 拒绝合法 TOML 浮点数:Codex 配置合并与安装回滚机制的源码级剖析 本篇以 .changeset/f
人工智能AI 应用提示工程开发工具工作流自动化AI Agentget-shit-done 配置键白名单机制解析:workflow._auto_chain_active 为何不再被 config-set 拒绝
get shit done 配置键白名单机制解析:workflow._auto_chain_active 为何不再被 config set 拒绝 本文以 get
人工智能AI 应用提示工程开发工具工作流自动化AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考