仓库说明写了「.env 不进入上下文」,助手仍然可能读它——提示词靠自觉,脚本才靠得住。但钩子不改变助手对任务的理解,只回答「这次动作放不放行」。
让助手去读一次 .env,它会被拒绝吗?换一台机器、换一种工具呢?
答「不确定」,说明拦截还停在提示词里。钩子只有真跑一次、看到拒绝信息时,才算存在。
钩子配置按工具落在三个位置,脚本统一从仓库根执行。Claude Code 没有独立配置文件,写在 settings.json 的 hooks 字段里。
仓库根目录/
├── .cursor/
│ ├── hooks.json # version: 1;事件名小写 preToolUse
│ └── hooks/
│ └── block-env.py # 从仓库根执行;stdin 读 JSON、stdout 写 JSON
├── .claude/
│ └── settings.json # 写在 hooks 字段里,不是 hooks.json;事件名 PreToolUse
└── .trae/
└── hooks.json # 事件名 PreToolUse;用 timeout 限时
判据落在位置上:Claude Code 的钩子写成独立的 hooks.json 不会被读;三处写错位置都不报错、只是不生效。配完要先实测一次:让助手去读一次 .env,看它是否被拒绝。
场景:排查「本地为什么连不上数据库」,助手的第一反应是读 .env 里的连接串——对排查来说这很合理,但正是要挡的动作。
.env,退出码 2,拒绝。助手收到拒绝原因,改读 .env.example,用里面的变量名继续排查。
判断只看一句:这个动作是「必须被挡住」还是「希望被记得」。前者才配钩子,后者有更合适的落点。
| 要做的事 | 做成什么 | 怎么判 |
|---|---|---|
| 读取密钥文件、写生产库、删除分支这类动作必须被挡住 | 钩子 | 一旦发生就必须拦下,靠不住自觉?是 → 钩子(这是它唯一的适用场景) |
| 希望助手每次记住一条做法 | 仓库说明 | 删掉它,不相关的任务会不会变差?会 → 仓库说明 |
| 只对某一类文件成立的约束 | 规则 | 这句话只在改这类文件时才用得到吗?是 → 规则 |
| 需要人主动触发的多步检查 | 命令 | 需不需要人记得去用、由人拍板?是 → 命令 |
| 要求合并前必须通过的检查 | 持续集成门禁 | 换台机器、换个人还要挡得住吗?要 → 门禁 |
钩子拦不住绕过:助手换用没覆盖的工具,或人在命令行里手工读取文件,钩子都不经过。所以它只降低概率,不构成门禁——真正兜底的是质量门禁里的密钥扫描。
三家的配置文件位置、事件名、失败时放不放行都不一样,写错位置不报错、只是不生效。规则文件本身也要一起保护:它能被助手改写,一条约束就被这次任务自己放开了。
| 工具 | 钩子配置 | 落地页 |
|---|---|---|
| Cursor | .cursor/hooks.json(failClosed 显式设 true,崩溃也不放行) | Cursor 钩子落地页 |
| TraeCode | .trae/hooks.json | TraeCode 钩子落地页 |
| Claude Code | .claude/settings.json 的 hooks 字段(不是 hooks.json) | Claude Code 钩子落地页 |