Skip to content

claude-code-hooks: pitfall #28 — the fallback target that's right for one reason and wrong for another - #253

Closed
daymade wants to merge 3 commits into
mainfrom
hooks-pitfall-28-misbound-target
Closed

claude-code-hooks: pitfall #28 — the fallback target that's right for one reason and wrong for another#253
daymade wants to merge 3 commits into
mainfrom
hooks-pitfall-28-misbound-target

Conversation

@daymade

@daymade daymade commented Aug 4, 2026

Copy link
Copy Markdown
Owner

起因

本 session 里 git-push-verify / git-commit-headcheck 连续五次报了错仓的 HEAD,措辞却是「权威源,勿信命令行内输出」——即它一边给出关于另一个仓库的正确事实,一边要求我放弃自己的观察。每次我都用 git -C 显式复核才没被带偏。

机制(喂真实 JSON 端到端测出来的,不是读代码推断)

命令形态 hook 报的仓 实际目标
git -C /字面路径 push 正确
R=/路径; git -C $R push 事件 cwd 另一个仓
git push(无 -C 事件 cwd

第 2 行与第 3 行输出逐字节相同(MD5 相等,独立评审与我各自测过)。根因:「命令没有显式目标」和「有显式目标但值读不到」退回了同一个兜底值,于是同一句 ✅ 既可能是最强判定、也可能是关于另一个仓的正确废话。而路径存在 shell 变量里根本不是奇技——恰恰是多仓 session 的常态,也就是这个缺陷最咬人的场景。

抽象出来的原则:兜底值必须携带「我为什么用兜底」的原因,因为不同原因下同一个值的可信度不同。

最该记的一层:藏住它的那个假设

两个 hook 的源码里都写着注释,断言这条路径「失效方向是 fail-loud,没有假绿路径」——因为「回退后的路径会读不到 HEAD」。这个判断被实测推翻:多仓 session 里回退到的事件 cwd 通常是另一个有效 git 仓,git log 成功,返回一个真实、可核验、完全无关的事实。假绿恰恰从「不可能假绿」这个假设里长出来。

领域锚(两条都不在原文件里)

  • confused deputy problem(Hardy 1988, ACM SIGOPS)的检查器变体:有权限作答的组件对「自己在为哪个对象作答」发生混淆。安全领域的诊断问句可直接移植 → 对 hook 就是「命令指名了哪个目标,我量的是不是它?」
  • alert fatigue 的量化:静态分析告警约 35%–91% 不被采取行动,部分工具误报率达 ~90%。这给「宁可说『我没查』也不要给自信的错答案」提供了可引用的成本依据。

不只是文档 —— 参考实现也修了

独立评审的头条发现是:我写的处方,被描述的那个 hook 并没有实现,而且它当时的行为正是处方第 2 条点名的反模式(把免责句放在 ✅ 之后)。文档在说一件实现没做的事,读者去看实现会抄到不合规版本。

所以两个生产 hook 一并修了(私有仓 daymade/scripts,commit 41c4227):给兜底标原因,「有显式目标但读不到」拒绝作答,且不借用「权威源 / 以本行为准」的措辞——那是测量挣来的,被绑定花掉;绑定是猜的时候不该借用。四形态端到端回归零误伤,并已在本 session 的真实调用路径上自证(变量形态→未核对,字面路径→正确判定)。

其余 findings(全部来自独立评审,已复现后修)

闸门

闸门 结果
Regression audit 0 candidates / 621 exact preservations(基线取自 git ref 非工作树)
Regression verify passed
quick_validate Skill is valid
Security scan 本次文件 0 命中
脱敏 0(先用已知必命中串标定 grep 再判)
hook 侧 bash -n ×2 + 四形态真实 JSON 端到端 + hook-health-check 静默通过

⚠️ 欠着的

评审跑的是修复前的版本;上述文档修改与两个 hook 的改动都没有再过一轮 fresh agent。我做了实跑验证,但那是自审。所以没有自己合并——请你决定是先补一轮还是直接合。

🤖 Generated with Claude Code

daymade and others added 3 commits August 4, 2026 21:51
…r that failed calibration

Adds a numbers section to SKILL.md plus scripts/scan_numeric_consistency.py.

The category matters because the dictionary structurally cannot reach it: a
dictionary rule needs a stable wrong->right mapping, and damaged digits have
none — one replacement corrupts "21", "5+1" and a date into three different
shapes. Three sub-classes get distinct settlement paths (magnitude, dropped
measure word, polarity inversion); polarity is called out as the dangerous one
and deliberately left un-automated, because the sentence stays grammatical and
only the meaning reverses.

The scanner's needle list is the dictionary's own to_text values — the strings
this toolchain writes into transcripts are exactly the ones that must never sit
inside a number, so no hand-maintained list is needed.

Three constraints came from calibration against a 222-transcript corpus rather
than from design, and each is documented where enforced: bounded 2-char
widening (unbounded fired 97x on healthy text vs 46 real), digit-before-term
only (the mirror direction is just how Chinese is written), metadata lines
skipped. Final: all four damage shapes caught, 0.004 findings/file.

CHANGELOG also records a detector that was built, calibrated and DELETED: the
magnitude-disagreement check fired 280 times with one true positive, because a
speaker contrasting two magnitudes is arithmetically identical to a dropped
zero. Narrowing on restatement markers made it worse. The negative result is
the reusable part — that discrimination needs the semantic read the native AI
pass already does, not lexical rules.

Independent review (fresh-context agent, artifact + reader spec only) raised 12
findings; 10 fixed, 1 kept with an explicit caveat, 1 declined. Notably it
caught that (?<![\w.]) matches CJK in Python 3, so "总共30+家" scanned clean
while "total 30+ customers" matched — invisible on this corpus because ASR put
spaces around numbers. Also fixed: silent clean report on a missing/corrupt
dictionary (now exit 2), exit-code collision between "crashed" and "found
candidates", comma-separated --domain matching the convention taught elsewhere,
and unsourced error-rate percentages replaced with a verifiable qualitative
claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… reproducible test suite

Second independent-review pass confirmed the earlier fixes and left four items.
Three are addressed here; the fourth (ROVER attribution unverified by the
reviewer) needs no code change — the citation carries name, institution and
year, which is what makes it checkable.

- The calibration claim was the one real gap: "222 transcripts, 0.004/file"
  cannot be re-run by a reader, because the corpus is private and cannot ship.
  Replaced by scripts/tests/test_numeric_consistency.py (22 cases), which pins
  the *behaviour* that measurement bought instead of the number: every damage
  shape is detected, and the healthy-input shapes that killed two earlier
  versions of this scanner stay silent. SKILL.md now points at the suite and
  says plainly which part is not reproducible and why.
- --json plus a hard failure printed nothing on stdout, so a caller parsing it
  unconditionally got an empty string and had to guess whether that meant
  "clean". It now emits {"_scan": {"error": ..., "needles_loaded": 0}} and
  still exits 2.
- "fetch_minute_audio.py gets you the audio to check that timestamp" left the
  last step unstated. Rewritten to route through the path this skill already
  has — wire the audio: frontmatter, enqueue the number, press Q in the review
  dashboard to hear that exact utterance — rather than implying a second
  mechanism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… for one reason and wrong for another

A verification hook derives its target from the command text and falls back to
the event cwd when it can't parse one. That fallback is correct for one of the
two reasons it fires and wrong for the other, and the code path is identical:
`git push` (no explicit target) legitimately adopts the cwd; `git -C "$R" push`
*names* a target the hook cannot resolve, adopts the same cwd, and renders a
confident verdict about a different repository. Both render byte-identically
(MD5-equal, measured), so neither hook nor reader can separate the honest
verdict from the misbound one.

Framed as the checking-tool form of the confused deputy problem (Hardy 1988):
a component with authority to answer becomes confused about which object it is
answering for. Fix: a fallback value must carry the reason it was chosen, and
only "no explicit target" earns a verdict.

Includes the assumption that hid it — both hooks carried a comment asserting
this path could only fail loudly "because the wrong path just fails to read".
That holds only if the fallback lands somewhere invalid; in a multi-repo
session it lands in another *valid* repo, the read succeeds, and returns a
real and entirely irrelevant fact. The false green grew out of the belief that
a false green was impossible.

Also, from the independent review:
- Meta-principle triage list indexed #1#22 only; #23#28 were unreachable by
  the document's own stated method ("match the symptom first"). Added routing
  for all six.
- #10 now forward-references #28#10's own generalization lists uninterpolated
  $VARS as in-class, but its fix (expanduser) is a no-op for them, so a reader
  stopping at #10 believes the class is covered.
- Noted the shared-library reassurance's boundary: it protects hooks that
  *source* the helper; one carrying its own inline parser inherits none of it,
  which is how #28 survived #10.
- Alert-fatigue figures given a searchable anchor instead of bare "published work".
- #28 now names the worked-example hooks; an entry you can't go read a
  before/after for is not verifiable.
- SKILL.md: removed a dangling "that table" whose nearest antecedent was an
  unrelated table in that file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@daymade

daymade commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

分支基线过时导致冲突(含一笔已随 #252 合并的重复 commit)。同内容用干净分支重开,见下方新 PR。

@daymade daymade closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant