ratchetRatchet
§ · Nested detail page

PostToolUse 到反馈的主循环 深度解析

这一页只看 guard loop。sources/ratchet/hooks/ratchet-guard.js:84 读 hook stdin,sources/ratchet/hooks/ratchet-guard.js:86 过滤非编辑工具,sources/ratchet/hooks/ratchet-guard.js:100 把 payload 转成 changes,sources/ratchet/hooks/ratchet-guard.js:117 调 detector,sources/ratchet/hooks/ratchet-guard.js:133 追加预算 finding,最后 sources/ratchet/hooks/ratchet-guard.js:155 把结果发回 host。这个流程让规则遵循检查发生在 agent 还能自我修正的时候。

§ 00 · IMPORT CHAIN

导入链与契约

这一页只看 guard loop。sources/ratchet/hooks/ratchet-guard.js:84 读 hook stdin,sources/ratchet/hooks/ratchet-guard.js:86 过滤非编辑工具,sources/ratchet/hooks/ratchet-guard.js:100 把 payload 转成 changes,sources/ratchet/hooks/ratchet-guard.js:117 调 detector,sources/ratchet/hooks/ratchet-guard.js:133 追加预算 finding,最后 sources/ratchet/hooks/ratchet-guard.js:155 把结果发回 host。这个流程让规则遵循检查发生在 agent 还能自我修正的时候。

src/ratchet/hooks/ratchet-guard.js · L45-68JavaScript
 45function changesFrom(payload, root) {
 46  const input = payload.tool_input || {};
 47  const filePath = input.file_path || input.path || input.notebook_path;
 48  if (!filePath) return [];
 49
 50  if (isManifest(filePath)) {
 51    const change = wholeFile(root, filePath);
 52    return change ? [change] : [];
 53  }
 54
 55  const edits = Array.isArray(input.edits) ? input.edits : [input];
 56  if (edits.some((edit) => typeof edit.old_string === 'string' || typeof edit.new_string === 'string')) {
 57    return edits.map((edit) => ({ path: filePath, before: edit.old_string || '', after: edit.new_string || '' }));
 58  }
 59
 60  const content = typeof input.content === 'string' ? input.content : input.file_text;
 61  if (typeof content !== 'string') return [];
 62  return [{ path: filePath, before: committedContent(root, filePath) || '', after: content }];
 63}
 64
 65function isNewFile(root, filePath, session) {
 66  if (session.newFiles.includes(relativePosix(root, filePath))) return false;
 67  return committedContent(root, filePath) === null;
 68}
§ 01 · CORE IMPL

核心实现,逐段拆

▸事件过滤与 diff 抽取

入口的关键约束在 sources/ratchet/hooks/ratchet-guard.js:84 到 sources/ratchet/hooks/ratchet-guard.js:101:没有 tool error、不是编辑工具、找不到文件路径都会提前返回。sources/ratchet/hooks/ratchet-guard.js:45 负责兼容 file_path/path/notebook_path,sources/ratchet/hooks/ratchet-guard.js:55 展开 MultiEdit,sources/ratchet/hooks/ratchet-guard.js:60 处理 Write。这些小分支保证 detector 拿到的是统一 change 结构。

src/ratchet/hooks/ratchet-guard.js · L45-68JavaScript
 45function changesFrom(payload, root) {
 46  const input = payload.tool_input || {};
 47  const filePath = input.file_path || input.path || input.notebook_path;
 48  if (!filePath) return [];
 49
 50  if (isManifest(filePath)) {
 51    const change = wholeFile(root, filePath);
 52    return change ? [change] : [];
 53  }
 54
 55  const edits = Array.isArray(input.edits) ? input.edits : [input];
 56  if (edits.some((edit) => typeof edit.old_string === 'string' || typeof edit.new_string === 'string')) {
 57    return edits.map((edit) => ({ path: filePath, before: edit.old_string || '', after: edit.new_string || '' }));
 58  }
 59
 60  const content = typeof input.content === 'string' ? input.content : input.file_text;
 61  if (typeof content !== 'string') return [];
 62  return [{ path: filePath, before: committedContent(root, filePath) || '', after: content }];
 63}
 64
 65function isNewFile(root, filePath, session) {
 66  if (session.newFiles.includes(relativePosix(root, filePath))) return false;
 67  return committedContent(root, filePath) === null;
 68}

▸检测、去重与阻断

主循环在 sources/ratchet/hooks/ratchet-guard.js:111 开始遍历 changes,sources/ratchet/hooks/ratchet-guard.js:121 过滤 baseline,sources/ratchet/hooks/ratchet-guard.js:124 用 fingerprint 去重已报告项。阻断逻辑集中在 sources/ratchet/hooks/ratchet-guard.js:146 到 sources/ratchet/hooks/ratchet-guard.js:152:只有 strict/block 且 certain 或预算超限才中止工具调用,其余模式使用 sources/ratchet/hooks/ratchet-guard.js:155 回灌建议。

src/ratchet/hooks/ratchet-guard.js · L111-158JavaScript
111  for (const change of changes) {
112    const relative = relativePosix(root, change.path);
113    if (relative.startsWith('..') || isTestPath(relative)) continue;
114    if (isNewFile(root, change.path, session)) session.newFiles.push(relative);
115    if (!session.touchedFiles.includes(relative)) session.touchedFiles.push(relative);
116
117    const result = inspect(change, context);
118    session.addedLines += result.added.length;
119    session.removedLines += result.removed.length;
120
121    const surviving = baseline.filter(result.findings, accepted, () => relative);
122    for (const finding of surviving) {
123      if (finding.dep && !session.deps.includes(finding.dep)) session.deps.push(finding.dep);
124      const key = fingerprint(finding, relative);
125      if (session.reported.includes(key)) continue;
126      session.reported.push(key);
127      finding.path = relative;
128      session.findings.push(finding);
129      fresh.push(finding);
130    }
131  }
132
133  const overruns = budget.enforce === 'silent' ? [] : budgetFindings(session, budget);
134  writeSession(session);
135
136  const shown = [...fresh, ...overruns].slice(0, MAX_REPORTED);
137  if (!shown.length) return;
138
139  const hidden = fresh.length + overruns.length - shown.length;
140  const body =
141    headline(session, budget, mode) +
142    '\n\n' +
143    alignedFindings(shown) +
144    (hidden > 0 ? '\n\n' + hidden + ' more not shown' : '');
145
146  const blocking =
147    budget.enforce === 'block' &&
148    (overruns.length > 0 || fresh.some((finding) => finding.confidence === CERTAIN));
149
150  if (blocking) {
151    block('PostToolUse', body + '\n\nStrict mode. Revert or shrink this change, or ask the user to raise the budget.');
152    return;
153  }
154
155  emit(
156    'PostToolUse',
157    body + '\n\nFix what applies, then continue. Wrong call? say so, or mark the line with: ratchet-ignore: reason'
158  );
§ 02 · KEY FLOW

关键流程

payload -> changes -> inspect -> baseline -> budget -> emit/block

数据流很短:sources/ratchet/hooks/ratchet-guard.js:103 准备符号上下文,sources/ratchet/hooks/ratchet-guard.js:108 读取 accepted baseline,sources/ratchet/hooks/ratchet-guard.js:117 生成 findings,sources/ratchet/hooks/ratchet-guard.js:136 控制最多展示 8 条,sources/ratchet/hooks/ratchet-guard.js:140 拼接 headline 和 aligned findings。这里的设计目标是低噪音即时反馈,而不是生成完整审计报告。

▸读源码时先看哪些行

建议先从 sources/ratchet/hooks/ratchet-guard.js:84 读到 sources/ratchet/hooks/ratchet-guard.js:100,确认 hook 输入如何变成 root、mode、budget 和 session;再跳到 sources/ratchet/hooks/ratchet-guard.js:111 到 sources/ratchet/hooks/ratchet-guard.js:129,看 change 如何被 inspect、baseline 和 session reported 去重消化;最后读 sources/ratchet/hooks/ratchet-guard.js:146 到 sources/ratchet/hooks/ratchet-guard.js:158,理解 strict mode 与普通 emit 的行为边界。

§ 03 · EDGE CASES

边界 / 失败模式

  • sources/ratchet/hooks/ratchet-guard.js:113 跳过测试路径,默认假设测试里的小 wrapper 和手写辅助逻辑可以更宽松。
  • sources/ratchet/hooks/ratchet-guard.js:133 budget.enforce 为 silent 时不会生成预算 finding,适合只记录不干预的团队迁移期。
  • sources/ratchet/hooks/ratchet-guard.js:150 strict block 的文案要求 revert 或缩小改动,避免 agent 在超预算状态继续扩大 diff。
Sub-page · 2026-08-05 Source · 0xwilliamortiz/ratchet ↑ ratchet 主页面