首页
/ 从形状正则到显式白名单:get-shit-done 安装器 3628 修复如何防止 Hook 误删

从形状正则到显式白名单:get-shit-done 安装器 3628 修复如何防止 Hook 误删

2026-09-04 18:57:40作者:袁立春Spencer

本文基于仓库变更集文档 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 明确指出受害对象有两类:

  1. 用户自写的自定义 hook,例如 hooks/gsd-personal-experiment.js
  2. 旧版本 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 规范化形态严格对齐(见下文测试断言)。

分类器的完整决策链:哪些文件自动处理,哪些交还用户

classifyPromptUserActioninstaller-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-skillkeep(用户自有内容必须保留)
官方自带 hook(#3628 白名单) BUNDLED_GSD_HOOK_FILES.has(relPath) bundled-gsd-hookremove
以上皆否 —— null(不自动分类,走人工确认)

修复后白名单分支的注释把安全边界写得很直白:与形状匹配但不在白名单内的文件(用户自写 hook、旧版本退役 hook)fall through 到 block-or-prompt 流程。

当白名单命中后,动作由 materializeResolutioninstaller-migration-report.cjs)具体化:

  • keepbaseline-preserve-user(幂等保留);
  • removebackup-and-remove——删除前先在迁移日志目录 gsd-migration-journal/<runId>-backups/ 留一份回滚备份。即使"自动删除",也不是物理抹除,这一点与 docs/installer-migrations.mdbackup-and-remove 动作类型的定义("用户会得到清晰报告并可检查备份")一致。

非交互(non-TTY)场景由 resolveInstallerMigrationPromptsForNonTtyinstaller-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 失败

  1. 白名单 → 磁盘方向test 第 66-79 行):遍历 BUNDLED_GSD_HOOK_FILES 每个条目,断言对应的 hooks/<文件名> 在磁盘上真实存在——防止"删除了某个 hook 文件却忘了从白名单移除";
  2. 磁盘 → 白名单方向test 第 81-93 行):用 readdirSync 扫描 hooks/ 下所有 gsd-*.{js,sh,cjs,mjs} 文件,断言每个都在白名单内——防止"新增发布 hook 却忘了加白名单"(后果是该 hook 在升级时反而会被当作未知文件阻塞,或者在旧版本逻辑下被误当用户文件);
  3. 结构约束test 第 37-64 行):白名单必须是 Set、非空,且每个条目都以 hooks/ 开头、使用 POSIX 斜杠、包含 gsd- 前缀。

测试还覆盖了负向与边界情形,确保修复没有破坏 #3610 既有边界(test 第 108-139 行):

  • hooks/gsd-personal-experiment.jshooks/gsd-my-custom-guard.shhooks/gsd-team-policy.cjshooks/gsd-retired-hook.jshooks/gsd-old-statusline.jshooks/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)只能回答"它看起来像官方文件",白名单才能回答"它就是官方文件"。其工程组合是:

  1. 单一事实来源:Object.freeze 的显式 Set 白名单,与发行版 hooks/ 目录一一对应;
  2. 分类器未命中即返回 null,把控制权交还 block-or-prompt 流程,用户文件永不静默消失;
  3. 即使命中白名单,删除路径也是 backup-and-remove,迁移日志保留回滚备份;
  4. 双向漂移守卫测试锁死"白名单 ↔ 磁盘发行物"的同步关系,任何一方改动都会立刻在 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

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384