首页
/ get-shit-done Graphify 自动更新 Hook 无法发布的根因与修复:从 `hooks/` 源码到 `~/.claude/hooks/` 的完整发布链路剖析(3579)

get-shit-done Graphify 自动更新 Hook 无法发布的根因与修复:从 `hooks/` 源码到 `~/.claude/hooks/` 的完整发布链路剖析(3579)

2026-09-07 18:46:40作者:柯茵沙

本篇技术指南围绕 changeset 档案 .changeset/fix-3579-graphify-hook-publish.md 展开,剖析 get-shit-done(GSD)中 Graphify 知识图谱自动更新 Hook gsd-graphify-update.sh 因发布链路断点而无法到达用户安装目录的问题,并结合 scripts/build-hooks.jsbin/install.js 的源码实现,讲解"源码 hook → hooks/dist/ 构建产物 → 运行时配置目录"三段式发布机制、两处根因修复(白名单补齐 + 子目录镜像)以及防回归的 coverage drift guard。读完你将理解 GSD 的 hook 打包与安装模型,掌握此类"文件从未进入发布物"类缺陷的系统性排查与封堵方法。

一、GSD 的 hook 发布模型:为什么需要一条三段式链路

GSD 内置了大量以 gsd-*.js / gsd-*.sh 命名的 Claude Code 等运行时 hook(PostToolUse、PreToolUse、SessionStart、Stop 等)。它们不可能直接从仓库源码目录被"引用式"使用,而是必须被打包、拷贝到用户运行时的配置目录(例如 ~/.claude/hooks/)。这一过程在源码上被拆成两段、共三个停留点:

  1. 源码层hooks/ 目录下平铺所有 hook 顶层文件,外加 lib/ 子目录存放被 hook 调用的"分离辅助脚本";
  2. 构建层scripts/build-hooks.js 把顶层 hook 与白名单子目录里的内容"物化"到 hooks/dist/ 下的构建产物,供 npm 包随发布携带(参见 tests/package-manifest.test.cjs 中对 "hooks" 必须列入 package.json files 的约束);
  3. 安装层bin/install.js 读取 hooks/dist/ 的扁平目录,将产物镜像拷贝到目标运行时的 hooks/ 目录,并完成版本头模板替换、可执行位设置、settings.json 钩子注册。

任何一层丢失文件,最终表现都一样:安装目标上不存在该 hook,而用户侧往往只有在功能不生效时才察觉到异常——正如本次 #3579 暴露的问题。

二、changeset 档案解读:Bug #3579 的问题定性

.changeset/fix-3579-graphify-hook-publish.md 是仓库使用的 changeset(变更集)记录,其 frontmatter 声明了 type: Fixedpr: 3579,正文对缺陷给出了精确定义:

gsd-graphify-update.sh 一直缺失于 scripts/build-hooks.jsHOOKS_TO_COPY 白名单,因此它从未进入 hooks/dist/;而安装器(bin/install.js)基于 hooks/dist/ 的扁平 readdirSync 循环拷贝,自然也不可能把它复制到 ~/.claude/hooks/

同时,hook 运行时需要的"分离重建辅助脚本" hooks/lib/gsd-graphify-rebuild.sh 同样被遗漏——因为 build-hooks.jsbin/install.js 两者此前只遍历顶层文件,从不深入子目录。

换句话说,这是一个单一表层症状、两层结构缺陷的经典发布事故:

  • 缺口 1(白名单缺失):构建脚本使用显式 allowlist,新增 hook 若忘记登记,构建层便静默跳过;
  • 缺口 2(目录深度受限):即使顶层 hook 打包成功,其依赖的 lib/ 子目录辅助脚本也不会被任何一层搬运。

三、被遗忘的主角:Graphify 自动更新 Hook 到底是什么

gsd-graphify-update.sh(见 hooks/gsd-graphify-update.sh)是一个 PostToolUse 的 Bash matcher hook:在主分支(HEAD)因 git 操作推进之后,自动在后台重建项目知识图谱,使后续 /gsd-graphify 相关查询(状态、查询、diff)始终基于最新提交的数据。它在功能上是"开箱即停"的选装件:

  • 默认关闭,即 hook 文件本身是 no-op;
  • 必须满足 .planning/config.json同时 存在 graphify.enabled: truegraphify.auto_update: true 才真正生效(docs/CONFIGURATION.mdgraphify.auto_update 默认值为 false,以保证升级后既有用户行为不变)。

从脚本头部的注释可见其设计为"8 道快速失败门",按开销从低到高依次拦截非目标场景(参见 hooks/gsd-graphify-update.sh):

  1. stdin 载荷存在且 tool_name == "Bash"
  2. 命令匹配 HEAD 推进类 git 操作:git commit / git merge / git pull / git rebase --continue / git cherry-pick,或等价的 gsd-sdk query commit 形态;
  3. $CI 为空(CI 环境抑制);
  4. 位于 git 仓库内;
  5. 当前分支 == 默认分支(git.base_branch 覆盖,否则在 main/master/trunk 中探测);
  6. 配置文件双重开关均开启;
  7. graphify 二进制在 PATH 上;
  8. 无重建在途(PID 锁,kill -0 探活,容忍陈旧锁)。

全部通过后,hook 会先同步.planning/graphs/.last-build-status.json 写入 status: "running" 的运行信号,随后通过兄弟目录定位 HOOK_DIR/lib/gsd-graphify-rebuild.sh,并以 disown 方式分离后台执行重建。关键点在于它依赖路径 $HOOK_DIR/lib/gsd-graphify-rebuild.sh——该辅助脚本与 hook 在目录结构上必须是"镜像相邻"关系,这正是修复中反复强调"子目录必须同步搬运"的根本原因。

四、根因拆解:链路两个断点的源码证据

断点一:构建白名单没有登记新 hook

在修复前,scripts/build-hooks.jsHOOKS_TO_COPY 数组是纯 JS hook 与既有 shell hook(gsd-session-state.shgsd-validate-commit.shgsd-phase-boundary.sh)的显式清单,并没有包含 gsd-graphify-update.sh。构建脚本随后用 fs.existsSync(src) 检查源文件是否存在,并对 .js 文件执行语法校验(new vm.Script(...) 防止历史上"重复 const 声明被发布"之类的事故),然后才把文件以"先写 staging 再 rename"的原子方式落入 hooks/dist/(见 scripts/build-hooks.js 顶部的注释与 renameAtomicWithRetry 实现)。白名单外的文件即使存在于 hooks/ 顶层,也不会进入 dist——该机制为防泄漏而设计,副作用是"新增即漏"。

断点二:构建与安装只遍历顶层文件

即使补上白名单,构建产物里也只会出现单个 hook 文件。gsd-graphify-update.shHOOK_DIR/lib/gsd-graphify-rebuild.sh 处查找分离重建辅助脚本,而该文件位于 hooks/lib/ 子目录(同目录还存放着供 .sh hook 调用的 git-cmd.js)。修复前的 build-hooks.js 主循环仅 path.join(HOOKS_DIR, hook) 处理顶层文件;bin/install.js 的安装循环同样只用 fs.readdirSync(hooksSrc) 得到的扁平条目 + isFile() 判断——于是 lib/ 中的内容在发布链路的每一站都被静默丢弃。

需要特别注意的是:安装器对顶层 .sh 文件并非简单复制,而是要逐字节做 {{GSD_VERSION}} 占位符替换(写入 gsd-hook-version 版本头,供 gsd-check-update 检测过期),并补 chmod 0o755 可执行位。子目录内的 .sh 若不被处理,就同时失去"版本戳 + 可执行位"两层保证,详见 bin/install.js 的 hook 拷贝循环。

五、修复落地:白名单、子目录镜像与注册校验

changeset 记载的修复共三条主线,均能在源码中逐一验证:

1. 把 hook 加入构建白名单

scripts/build-hooks.js 现在在 HOOKS_TO_COPY 尾部追加了带注释的条目:

// Graphify auto-update hook (#3347 / PR #3557 / #3579). Opt-in via
// .planning/config.json graphify.auto_update; off by default.
'gsd-graphify-update.sh'

2. 构建层新增白名单子目录复制

scripts/build-hooks.js 新增常量并接入主流程:

// Subdirectories under hooks/ whose contents must also ship to dist. Each
// entry is copied as `hooks/<dir>/*` → `hooks/dist/<dir>/*` so detached
// helpers (e.g. hooks/lib/gsd-graphify-rebuild.sh) resolve from the hook's
// installed runtime path. See #3579.
const HOOKS_SUBDIRS_TO_COPY = ['lib'];

构建主循环(scripts/build-hooks.js)随后会针对 lib 递归 readdirSync(srcDir, { withFileTypes: true }),仅复制文件条目,复用与顶层一致的"语法校验 → staging → rename 原子替换"逻辑,并将 .sh 的可执行位在首次可观察前就置好(chmodSync(stagedDest, 0o755))。产物因此变为 hooks/dist/gsd-graphify-update.sh + hooks/dist/lib/gsd-graphify-rebuild.sh

3. 安装层镜像子目录到目标

bin/install.jsreaddirSync(hooksSrc) 得到的目录条目分支处理:一层递归进入 lib/,将每个文件镜像写入目标 hooks/lib/;对 .sh 同样执行版本占位符替换与可执行位设置。这样 hook 在安装环境里通过 dirname "$0" 上溯后拼接 lib/gsd-graphify-rebuild.shREBUILD_SCRIPT 定位就始终成立。

此外,bin/install.js 的安装后校验清单 expectedShHooks 同步纳入了 'gsd-graphify-update.sh'(缺失时给出非致命警告),而 hook 的 settings.json 注册分支(bin/install.js)也只有在目标文件真实存在时才往 PostToolUse 事件推送 matcher: 'Bash' 的配置条目,否则会打印"Skipped graphify auto-update hook — gsd-graphify-update.sh not found at target"。三条防线相互印证,避免"文件缺失但注册成功"或"文件在但未注册"的隐性半残状态。

六、防回归:coverage drift guard 与回归测试网

changeset 强调最后补上了一道 coverage drift guard:此后 hooks/ 顶层每个 *.sh 都必须出现在 HOOKS_TO_COPY 中,杜绝再次出现"新增 hook 忘登记、静默不发版"。这道防线体现在回归测试上:

  • tests/graphify-visualization.test.cjs 新增了 #3579 的 Gap 1 与安装两段测试:先真实执行 build-hooks.js,断言每个顶层 hooks/*.sh 都被物化到 hooks/dist/(若未来有人忘加白名单,此用例会直接列出 missing 文件而失败);再断言 hooks/dist/gsd-graphify-update.shhooks/dist/lib/gsd-graphify-rebuild.sh 均存在;最后以 CLAUDE_CONFIG_DIR 指向临时目录的方式跑完整安装流程,验证两个文件都落地到目标 hooks 目录,且安装输出不再出现 "Missing expected hook" 或 "Skipped graphify auto-update hook" 警告。
  • tests/orphaned-hooks.test.cjs 则从另一方向守卫生态:gsd-check-update-worker.js 的受管 hook 清单必须与 build-hooks.jsHOOKS_TO_COPY 的 JS 条目一致,避免运行时更新器与发布清单产生认知分裂。
  • 历史回归基线 tests/bug-1834-sh-hooks-installed.test.cjs 早已验证三类社区 .sh hook 的部署、可执行位与 expectedShHooks 告警覆盖——#3579 相当于把同类保障平移到 graphify hook 与 lib/ 辅助脚本上。

七、从 Bug 中沉淀的工程原则

回顾 #3579,可以归纳出三条可复用的发布工程经验:

  1. 显式白名单需要配套"全量覆盖断言":只要发布采用 allowlist,就必然引入"漏登记"这一风险类别,必须用测试把"源目录实际文件 ⊆ 白名单"固化为不可绕过的约束;
  2. 路径寻址的辅助文件必须与主文件同步镜像:凡 hook 通过相对路径(dirname "$0" 上溯)加载兄弟辅助脚本,构建与安装两层都必须保留目录结构,否则会出现"顶层文件在、运行期依赖缺失"的隐蔽故障;
  3. 版本戳与可执行位是 .sh hook 的隐形契约:安装层对 .sh{{GSD_VERSION}} 替换与 0o755 处理不可省略,否则更新器无法识别过期 hook、运行时直接以不可执行文件启动而报错。

对使用者而言,本修复是透明的:升级到包含 #3579 的版本后重新执行 npx get-shit-done-cc --claude --global(或对应运行时参数),即可在 ~/.claude/hooks/ 看到 gsd-graphify-update.shhooks/lib/gsd-graphify-rebuild.sh;只有当你需要让提交自动驱动知识图谱重建时,才需在 .planning/config.json 中显式开启 graphify.enabled: truegraphify.auto_update: true,其余场景 hook 始终保持零成本空转。

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

项目优选

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