GateGuard — 事实前置操作门控 是做什么的?
一个 PreToolUse 钩子,强制 Claude 在编辑前进行调查。与自我评估(“你确定吗?”)不同,它要求提供具体的事实。调查行为本身就能产生自我评估从未实现的认知效果。
何时启用
- 在文件编辑会影响多个模块的任何代码库上工作
- 项目包含具有特定模式或日期格式的数据文件
- 团队要求 AI 生成的代码必须匹配现有模式
- 任何 Claude 倾向于猜测而非调查的工作流
核心理念
LLM 自我评估不起作用。问“你是否违反了任何政策?”答案总是“否。”这一点已通过实验验证。
但问“列出导入此模块的所有文件”会强制 LLM 执行 Grep 和 Read。调查本身会创建上下文,从而改变输出。
三阶段门控:
1. 拒绝(DENY)—— 阻止首次 Edit/Write/Bash 尝试
2. 强制(FORCE)—— 告诉模型需要收集哪些确切事实
3. 允许(ALLOW)—— 事实呈现后允许重试
没有竞争对手能做到全部三个阶段。大多数只停留在拒绝。
证据
两次独立的 A/B 测试,相同的代理,相同的任务:
| 任务 | 带门控 | 无门控 | 差距 |
|---|---|---|---|
| 分析模块 | 8.0/10 | 6.5/10 | +1.5 |
| Webhook 验证器 | 10.0/10 | 7.0/10 | +3.0 |
| 平均 | 9.0 | 6.75 | +2.25 |
两种代理都能生成可运行且通过测试的代码。区别在于设计深度。
门控类型
编辑 / 多编辑门控(每个文件的首次编辑)
多编辑的处理方式相同——批次中的每个文件单独被门控。
在编辑 {file_path} 之前,请呈现以下事实:
1. 列出导入/引用此文件的所有文件(搜索整个目录树——使用 Glob/Grep,或通过 Bash 使用 find/grep)
2. 列出受此更改影响的公共函数/类
3. 如果此文件读取/写入数据文件,则显示字段名称、结构
和日期格式(使用脱敏或合成值,而非原始生产数据)
4. 逐字引用用户的当前指令
写入门控(首次新建文件)
在创建 {file_path} 之前,请呈现以下事实:
1. 命名将调用此新文件的文件及行号
2. 确认没有现有文件提供相同功能(搜索整个目录树——使用 Glob/Grep,或通过 Bash 使用 find/grep)
3. 如果此文件读取/写入数据文件,则显示字段名称、结构
和日期格式(使用脱敏或合成值,而非原始生产数据)
4. 逐字引用用户的当前指令
破坏性 Bash 门控(每次破坏性命令)
触发条件:rm -rf、git reset --hard、git push --force、drop table 等。
1. 列出此命令将修改或删除的所有文件/数据
2. 编写一行回滚过程
3. 逐字引用用户的当前指令
常规 Bash 门控(每个会话一次)
1. 用一句话说明当前用户请求
2. 此具体命令验证或生成的内容
并行批处理和部分应用
首次触达门控独立评估每个工具调用。当多个针对尚未触达文件的编辑在单个并行批次中发送时,第一个调用被拒绝,拒绝标记该文件为已检查,因此该批次中的兄弟编辑会被应用。不会回滚:文件可能最终包含兄弟编辑内容,而不包含被拒绝的编辑内容。
拒绝消息会命名该文件,并警告批次兄弟可能已被应用。请按以下方式处理:
- 将依赖性的编辑(如定义及其首次使用,或导入及其调用点)发送到尚未触达的文件时,应顺序发送,而非放在同一并行批次中。
- 在首次触达被拒绝后,呈现事实,重试被拒绝的编辑,并在基于批次中的其他内容进行构建之前重新读取该文件。
不可能实现批次级锁定:钩子一次只能看到一个工具调用,因此门控无法知道哪些调用是同时到达的。
快速开始
选项 A:使用 ECC 钩子(零安装)
位于 scripts/hooks/gateguard-fact-force.js 的钩子已包含在此插件中。通过 hooks.json 启用。
如果 GateGuard 阻止了设置或修复工作,请以 ECC_GATEGUARD=off 启动会话。如需钩子级控制,请继续使用 ECC_DISABLED_HOOKS 并指定 GateGuard 钩子 ID。
在长时间会话中,只有前 GATEGUARD_FACT_FORCE_FULL_DENIALS 次(默认 3 次)事实强制拒绝会发出完整的四事实块;之后的拒绝会压缩为一行,携带拒绝序号,以避免几乎相同的块堆积在上下文窗口中并放大模型重复循环(#2142)。在呈现事实后重试同一文件或命令永远不会重新触发门控。
分级控制
ECC_GATEGUARD=off(或 GATEGUARD_DISABLED=1)完全关闭门控。下表中的变量不会完全关闭门控——每个变量只会缩窄某一行为,而关键性的破坏性 Bash 检查仍会持续运行:
| 变量 | 默认值 | 效果 |
|---|---|---|
GATEGUARD_BASH_ROUTINE_DISABLED | 未设置(门控开启) | 仅禁用常规 Bash 门控。破坏性 Bash 门控(rm -rf、git reset --hard、drop table、dd if= 等)不受影响。 |
GATEGUARD_EXEMPT_GLOBS | 未设置(无豁免) | 逗号分隔的 glob 模式;匹配的 Edit/Write/MultiEdit 目标会跳过首次触达的事实强制。适用于低导入价值的目录树(测试文件、生成产物、临时目录),其中“谁导入此文件/什么模式”不具信号意义。 |
GATEGUARD_FACT_FORCE_FULL_DENIALS | 3 | 多少次拒绝会发出完整的四事实块,之后压缩为一行。设置为 0 则从第一次拒绝就开始压缩。 |
GATEGUARD_BASH_EXTRA_DESTRUCTIVE | 未设置 | 额外的破坏性命令模式,以正则表达式的源字符串形式,添加到内置集合中。格式错误的正则被视为未设置(内置模式仍生效),并向 stderr 记录一次日志。 |
GATEGUARD_STATE_DIR | ~/.gateguard | 每个会话的门控状态存储位置。如果状态无法持久化,门控会放行操作以避免循环,并在警告中引用此变量。 |
GATEGUARD_BASH_ROUTINE_DISABLED 接受 1、true、on、enabled、enable 或 yes(不区分大小写和空白);任何其他值都保持门控开启。
完全关闭门控
| 变量 | 效果 |
|---|---|
ECC_GATEGUARD=off | 禁用当前会话的 GateGuard。接受 0、false、off、disabled 或 disable。 |
GATEGUARD_DISABLED=1 | 效果相同。仅识别 1——上述拼写不适用。 |
如需钩子级控制,请继续使用 ECC_DISABLED_HOOKS 并指定 GateGuard 钩子 ID。
GATEGUARD_EXEMPT_GLOBS 的 glob 语义
模式匹配整个项目相对目标路径。项目根目录为 CLAUDE_PROJECT_DIR,回退到钩子负载中的 cwd,再回退到钩子进程工作目录。相对 glob 永远不会豁免该根目录之外的目标。显式的绝对 glob 匹配整个绝对目标路径,可能故意豁免项目之外的路径。
模式和路径均使用 / 分隔符并执行小写匹配。* 匹配一个段内,** 跨段匹配,? 匹配一个非分隔符字符。**/ 包含零个目录,因此 **/tests/** 也会匹配 tests/foo.js。格式错误的模式会被丢弃,不授予豁免。
自 2.2.1 起,services/** 仅覆盖项目根目录下的 services 目录树,*.md 仅覆盖根目录下的 Markdown 文件。请使用 **/*.md 匹配项目内所有 Markdown 文件。现有未锚定的豁免可能需要调整:
{
"env": {
"GATEGUARD_BASH_ROUTINE_DISABLED": "1",
"GATEGUARD_EXEMPT_GLOBS": "**/tests/**,tests/**,**/*.test.*,**/docs/**,**/dist/**"
}
}
选项 B:包含配置的完整包
pip install gateguard-ai
gateguard init
这会添加 .gateguard.yml 用于每个项目的配置(自定义消息、忽略路径、门控开关)。
反模式
- 不要使用自我评估代替。 “你确定吗?”总是得到“是的。”这一点已通过实验验证。
- 不要跳过数据模式检查。 两个 A/B 测试代理都假设使用 ISO-8601 日期格式,而实际数据使用
%Y/%m/%d %H:%M。检查数据结构(使用脱敏值)可以防止这类错误。 - 不要对每个 Bash 命令都开门控。 常规 Bash 门控每个会话只触发一次。破坏性 Bash 门控每次都触发。这种平衡既能避免减慢速度,又能捕获真正风险。
最佳实践
- 让门控自然触发。不要试图预先回答门控问题——调查本身才是提高质量的关键。
- 针对你的领域自定义门控消息。如果你的项目有特定约定,请添加到门控提示中。
- 使用
.gateguard.yml忽略诸如.venv/、node_modules/、.git/等路径。
相关技能
safety-guard— 运行时安全检查(互补,不重叠)code-reviewer— 编辑后审查(GateGuard 是编辑前调查)