从形状正则到显式白名单:get-shit-done 安装器 3628 修复如何防止 Hook 误删
本文基于仓库变更集文档 3628-bundled-hook-whitelist.md(类型标记为 Security,关联 issue #3628),解析 get-shit-done 安装器迁移层的一次关键安全修复:bundled-gsd-hook 分类器为何不能依赖"命名形状正则"自动删除用户目录下的 hooks/gsd-* 文件,以及显式白名单 BUNDLED_GSD_HOOK_FILES + CI 漂移守卫如何保证"自动删除范围 == 实际发布文件集合"。读完本文,你能理解该安装器 prompt-user 阻塞流程的完整决策链,并掌握"白名单 + 双向漂移测试"这一可复用的防误删模式。
背景:#3610 修复了"升级中断",却引入了"静默误删"
要理解 #3628,需要先理解它修补的对象。get-shit-done 提供一层**安装器迁移(installer migration)**架构(见 docs/installer-migrations.md),其设计目标第一条就是"Protect user data by default",并区分三类文件:
- Managed file:GSD 安装并记录在安装清单(manifest)中的文件,未被修改时可自动替换;
- User-owned file:用户自己创建的文件,"绝不能因为恰好位于 GSD 目录下就被删除";
- Unknown file:既不在清单中、又未被归类为用户所有的文件,除非有明确证据被迁移显式归类,否则一律保留。
此前 #3610 解决了一个"升级硬中断"问题:全新执行 npx get-shit-done-cc@latest --codex 时,目标目录(如 ~/.codex/hooks/)里残留着上一版本 GSD 自带的 hook 文件,安装器清单无法证明这些文件"受管理",于是被归类为"GSD 风格文件但无清单证据,需要用户显式选择",assertInstallerMigrationsUnblocked 直接抛错终止升级(见 tests/bug-3610-installer-migration-bundled-hooks-classification.test.cjs 的文件头注释)。
#3610 的修复是在分类器 classifyPromptUserAction 中新增 bundled-gsd-hook 类别:凡是形如 hooks/gsd-<name>.{js,sh,cjs,mjs} 的文件都自动判定为"官方自带 hook",默认为 remove(安装器随后会写入新版本覆盖)。问题在于它使用的是形状正则:
/^hooks\/gsd-[^/]+\.(?:js|sh|cjs|mjs)$/
这条正则匹配的是"任何符合该命名形状的文件",而不是 npm 包中实际发布的 13 个 hook。变更集文档 3628-bundled-hook-whitelist.md 明确指出受害对象有两类:
- 用户自写的自定义 hook,例如
hooks/gsd-personal-experiment.js; - 旧版本 GSD 发布、但当前版本已退役的 hook 文件。
这两类文件在首次基线扫描(first-time-baseline scan)时会被静默自动分类并删除——属于无声的数据丢失(silent data loss),这正是该变更集被标记为 type: Security 的原因。
修复实现:从"匹配形状"改为"白名单精确命中"
修复落在 get-shit-done/bin/lib/installer-migration-report.cjs。分类器不再依赖正则形状,而是查询一个冻结的显式白名单集合,与 npm 发行版中 hooks/ 目录下实际发布的 13 个文件一一对应:
// #3628: explicit whitelist of bundled hook files shipped in the npm
// distribution under `hooks/`.
const BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
'hooks/gsd-check-update-worker.js',
'hooks/gsd-check-update.js',
'hooks/gsd-context-monitor.js',
'hooks/gsd-graphify-update.sh',
'hooks/gsd-phase-boundary.sh',
'hooks/gsd-prompt-guard.js',
'hooks/gsd-read-guard.js',
'hooks/gsd-read-injection-scanner.js',
'hooks/gsd-session-state.sh',
'hooks/gsd-statusline.js',
'hooks/gsd-update-banner.js',
'hooks/gsd-validate-commit.sh',
'hooks/gsd-workflow-guard.js',
]));
可以核对仓库根目录 hooks/ 目录:其中 13 个 gsd-*.{js,sh} 文件与白名单完全一致,另有 lib/ 子目录(两个辅助文件),不会被 [^/]+ 的形状约束误匹配。
两个实现细节值得注意:
Object.freeze(new Set([...])):白名单是模块级不可变常量("single point of truth"),测试会断言它必须以Set类型导出,保证调用方可以用has()做 O(1) 成员探测,且运行期无法被意外篡改;- 白名单条目强制使用 POSIX 斜杠且带
hooks/前缀,与分类器接收的relPath规范化形态严格对齐(见下文测试断言)。
分类器的完整决策链:哪些文件自动处理,哪些交还用户
classifyPromptUserAction(installer-migration-report.cjs)对每个被阻塞的 prompt-user 动作按 relPath 做三类判定,未命中任何一类则返回 null,落入既有的"阻塞或提示"(block-or-prompt)流程,用户保留控制权:
| 判定 | 条件 | 返回类别 / 默认选择 |
|---|---|---|
| 过期的 SDK 构建产物 | relPath 匹配 `^get-shit-done/sdk/(dist |
src)/` |
| 用户可见的 skill 锚点 | relPath 匹配 ^skills/gsd-[^/]+/SKILL\.md$ |
user-facing-skill → keep(用户自有内容必须保留) |
| 官方自带 hook(#3628 白名单) | BUNDLED_GSD_HOOK_FILES.has(relPath) |
bundled-gsd-hook → remove |
| 以上皆否 | —— | null(不自动分类,走人工确认) |
修复后白名单分支的注释把安全边界写得很直白:与形状匹配但不在白名单内的文件(用户自写 hook、旧版本退役 hook)fall through 到 block-or-prompt 流程。
当白名单命中后,动作由 materializeResolution(installer-migration-report.cjs)具体化:
keep→baseline-preserve-user(幂等保留);remove→backup-and-remove——删除前先在迁移日志目录gsd-migration-journal/<runId>-backups/留一份回滚备份。即使"自动删除",也不是物理抹除,这一点与 docs/installer-migrations.md 中backup-and-remove动作类型的定义("用户会得到清晰报告并可检查备份")一致。
非交互(non-TTY)场景由 resolveInstallerMigrationPromptsForNonTty(installer-migration-report.cjs)驱动:当安装器无法交互式提问时,优先读取环境变量 GSD_INSTALLER_MIGRATION_RESOLVE(取值 keep/remove)作为操作者覆写;未设置时才走上述分类器安全默认值。无法安全默认的动作会被过滤回 result.blocked,最终由 assertInstallerMigrationsUnblocked 抛出按原因分组的错误信息(每组最多展示 3 条示例路径),并提示"设置 GSD_INSTALLER_MIGRATION_RESOLVE=<choice> 可在非交互环境下解决"。换言之,白名单机制把"可无脑处理的文件"和"必须人来拍板的文件"切开了:白名单内的 13 个文件自动清理,白名单外的一切文件升级不会静默动、而是阻塞并给出可操作的解决途径。
CI 漂移守卫:白名单必须与磁盘上的 hooks/ 双向一致
变更集文档强调了配套防线:tests/bug-3628-bundled-hook-classifier-whitelist.test.cjs 是一个"漂移守卫",任何方向的漂移都会让 CI 失败:
- 白名单 → 磁盘方向(test 第 66-79 行):遍历
BUNDLED_GSD_HOOK_FILES每个条目,断言对应的hooks/<文件名>在磁盘上真实存在——防止"删除了某个 hook 文件却忘了从白名单移除"; - 磁盘 → 白名单方向(test 第 81-93 行):用
readdirSync扫描hooks/下所有gsd-*.{js,sh,cjs,mjs}文件,断言每个都在白名单内——防止"新增发布 hook 却忘了加白名单"(后果是该 hook 在升级时反而会被当作未知文件阻塞,或者在旧版本逻辑下被误当用户文件); - 结构约束(test 第 37-64 行):白名单必须是
Set、非空,且每个条目都以hooks/开头、使用 POSIX 斜杠、包含gsd-前缀。
测试还覆盖了负向与边界情形,确保修复没有破坏 #3610 既有边界(test 第 108-139 行):
hooks/gsd-personal-experiment.js、hooks/gsd-my-custom-guard.sh、hooks/gsd-team-policy.cjs、hooks/gsd-retired-hook.js、hooks/gsd-old-statusline.js、hooks/gsd-experimental.mjs这 6 个用户自有/退役样例必须返回null(不得自动分类);- 嵌套目录
hooks/gsd-helpers/index.js不得被分类(保留 #3610 引入的层级边界); - 非
gsd-前缀的hooks/my-custom-hook.js不得被分类。
正向测试则遍历白名单全部 13 项,断言每一项的分类结果恰好是 { category: 'bundled-gsd-hook', choice: 'remove' }。
小结:一条值得借鉴的"自动删除安全边界"
#3628 的教训可以浓缩为一句话:任何"自动删除/覆盖"逻辑的判定依据必须是"发行版实际发布物清单",而不是"命名形状相似性"。形状正则(hooks/gsd-*.js)只能回答"它看起来像官方文件",白名单才能回答"它就是官方文件"。其工程组合是:
- 单一事实来源:
Object.freeze的显式Set白名单,与发行版hooks/目录一一对应; - 分类器未命中即返回
null,把控制权交还 block-or-prompt 流程,用户文件永不静默消失; - 即使命中白名单,删除路径也是
backup-and-remove,迁移日志保留回滚备份; - 双向漂移守卫测试锁死"白名单 ↔ 磁盘发行物"的同步关系,任何一方改动都会立刻在 CI 暴露。
相关延伸阅读:安装器迁移架构 docs/installer-migrations.md、非 TTY 解析的早期规格变更集 3541-installer-migration-prompt-user-resolution.md、上游问题修复测试 tests/bug-3610-installer-migration-bundled-hooks-classification.test.cjs 与 #3610 变更集 3610-codex-install-bundled-hooks-blocker.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00