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 |
两个代理都能生成可运行且通过测试的代码。区别在于设计深度。
门控类型
编辑 / MultiEdit 门控(每个文件的首次编辑)
MultiEdit 的处理方式相同——批次中的每个文件都被单独门控。
在编辑 {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 启动会话。对于钩子级别的控制,请继续使用带有 GateGuard 钩子 ID 的 ECC_DISABLED_HOOKS。
在长会话中,只有前 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;匹配的编辑/写入/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 — 上述拼写形式在此处不适用。 |
对于钩子级别的控制,请继续使用带有 GateGuard 钩子 ID 的 ECC_DISABLED_HOOKS。
GATEGUARD_EXEMPT_GLOBS 的 Glob 语义
模式匹配整个项目相对目标路径。项目根目录为 CLAUDE_PROJECT_DIR,回退到钩子 payload 的 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 是编辑前调查)