跳转至

SOP:glob-grep-guard 升级为「强制拦截」(2026-10-07)

1. 背景

规则写明「检索与列举:限定目录、排除依赖目录、探一级目录用 ls -1d */、结果设上限」,但复测证伪了条款效力:

  • **/* 被 .venv 污染至少复现 2 次(其中一次撞 200 条结果上限);
  • LS 根目录「连犯两次」(4 万字符截断)。

根因与 merge-guard 同一条——缺必然触发点。该 Hook 原本只做提醒;本次按用户要求("能强制的强制")升级为强制拦截,与 merge-guard 用同一套机制。

2. 目标

  1. 判据 G(新):LS 且 path 为空或过浅(/、~、项目根及其上两级)→ 强制;
  2. 判据 H(升级):Glob/Grep 的 pattern/glob 以 ** 或 * 开头且未给 path → 强制(原为提醒);
  3. 复用 merge-guard 的 JSON deny + 指纹放行 + 每会话上限 3 次;
  4. 误判只浪费一次往返,绝不瘫痪会话。

3. 前置条件

  • 备份点:backup/glob-grep-guard/glob-grep-guard.py.bak.20261007-005534、backup/trae-cn/hooks.json.bak.20261007-005745;
  • 已知:改脚本即时生效;改 hooks.json 须重启 Trae。

4. 影响范围

对象 改动 生效方式
glob-grep-guard/glob-grep-guard.py 重写(判据 G/H + 强制 + 放行) 即时
~/.trae-cn/hooks.json PreToolUse 新增 matcher: "LS" 指向同一脚本 须重启 Trae

影响面:Glob / Grep / LS 三类工具调用。不涉及服务与在跑作业。

5. 分步操作

步骤 1:备份

cd /home/rain/Documents/services/token_proxy
TS=$(date +%Y%m%d-%H%M%S)
mkdir -p glob-grep-enforce-20261007/backup/{glob-grep-guard,trae-cn}
cp -p glob-grep-guard/glob-grep-guard.py glob-grep-enforce-20261007/backup/glob-grep-guard/glob-grep-guard.py.bak.$TS
cp -p ~/.trae-cn/hooks.json            glob-grep-enforce-20261007/backup/trae-cn/hooks.json.bak.$TS
chmod 600 glob-grep-enforce-20261007/backup/*/*.bak.*

预期:两份 .bak,权限 600。

步骤 2:改脚本

坑 1(关键):LS 判据写了也没用——hooks.json 里 glob-grep-guard 只注册了 matcher: "Glob" 与 "Grep",不含 LS。首轮真机验证因此"没被拦",排查 state 才发现 Hook 根本没被调用。判据能否生效取决于 matcher,不取决于脚本。

坑 2:放行分支必须单独处理。首版把"指纹命中的原样重发"落到了提醒分支 → 重发虽被放行,但会多注入一条提醒(回归用例 R1 失败)。

坑 3:缺 session_id 时绝不能 deny——无状态则放行机制失效,deny 会变成死锁。此时只提醒。

等价示意:

python3 - <<'PY'
src = open('glob-grep-guard/glob-grep-guard.py').read()
for token in ('TARGET_TOOLS = {"Glob", "Grep", "LS"}',
              'SHALLOW_PATHS', 'def _emit_deny', 'if fp in denied'):
    assert token in src, token
print('判据 G/H 与强制通道已就位')
PY

步骤 3:改 hooks.json(新增 LS matcher)

python3 - <<'PY'
import json, os
p = os.path.expanduser("~/.trae-cn/hooks.json")
h = json.load(open(p, encoding="utf-8"))
pre = h["hooks"]["PreToolUse"]
if not any(it.get("matcher") == "LS" for it in pre):
    tpl = next(it["hooks"][0] for it in pre if it.get("matcher") == "Glob")
    pre.append({"matcher": "LS", "hooks": [dict(tpl)]})
    json.dump(h, open(p, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
    print("已加入 LS matcher")
else:
    print("已存在 LS matcher,跳过(幂等)")
PY

预期:已加入 LS matcher;复查 PreToolUse 应为 6 条(Read / Glob / Grep / WebFetch / * / LS)。

步骤 4:离线回归

见第 6 章用例表。

步骤 5:真机验证

  • 判据 H 可立即验证(matcher 已在):Glob(pattern="**/*") 应被拦;
  • 判据 G 须重启 Trae 后验证:LS(path=<项目根>) 应被拦。

6. 验证

# 用例 判据 结果
G1 LS 项目根 deny 通过
G2 LS 家目录 deny 通过
G3 LS 无 path deny 通过
G4 LS 具体子目录 不触发 通过
H1 Glob **/* 无 path deny 通过
H2 Grep **/*.py 无 path deny 通过
H3 Glob docs/**/*.md(已限定写法) 不触发 通过
H4 Glob **/* 带 path 不触发 通过
R1 deny 后原样重发 静默放行 通过
R2 缺 session_id 仅提醒、不拦 通过
R3 非法 JSON 静默 exit 0 通过
R4 连续 4 次不同违规 deny 恰 3 次(上限) 通过
R5 非目标工具(Read) 静默 通过

离线合计 13/13 通过;merge-guard 同步改"静默放行"后回归 8/8。 真机:判据 H 已实测拦截成功;判据 G 待重启后验证。

7. 回滚

备份位置:glob-grep-enforce-20261007/backup/(台账见其 README.md)。

cd /home/rain/Documents/services/token_proxy
cp glob-grep-enforce-20261007/backup/glob-grep-guard/glob-grep-guard.py.bak.20261007-005534 glob-grep-guard/glob-grep-guard.py
cp glob-grep-enforce-20261007/backup/trae-cn/hooks.json.bak.20261007-005745 ~/.trae-cn/hooks.json   # 改回后须重启 Trae

回滚后验证:Glob "**/*" 与 LS 项目根 均只提醒、不拦截(回到旧行为)。 回滚触发判据:误拦合法的定向检索、或拦截导致任务失败。

8. 风险说明

风险 缓解
误拦合法的全库检索 放行机制(原样重发即过)+ 每会话上限 3 次
SHALLOW_PATHS 判定过宽/过窄 仅含 /、~、项目根及其上两级;子目录一律不拦
缺 session_id 时 deny 会死锁 该分支只提醒、不 deny
改了 hooks.json 却忘记重启 本 SOP 与 HANDOFF 均标注;重启前判据 G 静默不生效(无报错)
判据静默失效(本次已踩) matcher 与判据须成对核验;离线回归必须覆盖"该触发 / 不该触发"两侧

附录:交付物

文件 说明
glob-grep-guard/glob-grep-guard.py 被改实现体(判据 G/H + 强制 + 放行)
~/.trae-cn/hooks.json 新增 LS matcher
backup/ 回滚点(2 份,600),台账见 backup/README.md
docs/BASELINE.md §2 Hook 清单更新(glob-grep-guard 升强制)
docs/HANDOFF.md §3 重启待办 + 待验证项

来源:/home/rain/Documents/services/token_proxy/glob-grep-enforce-20261007/SOP.md | 最后更新:2026-10-07 00:58(由 build/sync.py 同步,请勿在站点侧修改)