本页判断标准

在对话里故意让助手读一次 .env,脚本会在动作发生前把它拦住吗?

答「不确定」,说明配完之后一次都没真踩过。合并这个钩子之前,先在终端自己触发一次拒绝,看到那句提示再合。

钩子的配置形状与退出码

说明性的约定仍写在 AGENTS.md 里,脚本不负责讲道理。钩子只回答一个问题:这次动作放不放行。Cursor 的配置里 command 与 failClosed 是平铺的两项,没有 matcher,也没有嵌套的 hooks 数组——要按工具名区分,就把判断写进脚本。

退出码 0
脚本成功
要拒绝,就向标准输出写 {"permission":"deny", …},再返回 0。
退出码 2
拒绝
把原因写在标准输出的 JSON 里,或直接以退出码 2 拒绝。
其他非 0
默认放行
脚本崩溃、语法错误都会走到这里。要拦住它们,必须显式设 failClosed: true。
可以这么做 在配置里写 command 和 failClosed: true;把判断逻辑(工具名、路径)放进脚本,读标准输入的 JSON;返回 permission: deny 或退出码 2。
不要这么做 把事件名写成 PreToolUse;在 hooks.json 里写 matcher;不设 failClosed;把脚本放到仓库外,或写相对位置不确定的路径。

钩子从仓库根目录执行,所以 command 里的相对路径都相对仓库根。先在仓库根跑一次 python3 --version:解释器不在,脚本根本起不来,效果等同于没配。什么时候不用钩子:这件事只需要提醒、不需要拦住时,把它写进 AGENTS.md 或规则就够了。

钩子在三处

钩子是三套工具差异最大的一层:配置文件放在哪、事件名怎么写、有没有 matcher、失败时放不放行,四项全不一样。从别的工具搬过来时逐行核这张表。

项Claude CodeCursorTraeCode
项目配置.claude/settings.json 的 hooks 字段(不是 hooks.json).cursor/hooks.json,version: 1.trae/hooks.json,version 目前只用 1
本地、不提交.claude/settings.local.json————
用户级~/.claude/settings.json——~/.trae-cn/hooks.json(本机所有项目共用)
事件名PreToolUsepreToolUse(小写 p)PreToolUse
matcher匹配工具名,如 Bash|Write|Edit配置里不写 matcher,判断逻辑放进脚本matcher 是正则,匹配工具名:RunCommand、Read、Write、Edit
配置形状matcher 加 hooks: [{type, command}] 两层command 与 failClosed 平铺,无 matcher、无嵌套matcher 加 hooks: [{type, command, timeout}] 两层
拒绝方式退出码 0 且标准输出输出 permissionDecision: deny 的 JSON;或退出码 2(原因写标准错误)退出码 2;或退出码 0 且标准输出输出 {"permission":"deny", …}退出码 0 且标准输出输出 permissionDecision: deny 的 JSON;或退出码 2(标准错误交给模型)
其他退出码不拦截默认放行不拦截
脚本崩溃或超时failClosed 默认 false,须显式设 true 才不放行failClosed 默认 false,须显式设 true没有 failClosed;脚本崩溃等于没设这个钩子
超时————timeout 不写默认 30 秒
执行目录仓库根,命令里用 "$CLAUDE_PROJECT_DIR" 定位脚本仓库根——
额外能力permissions.deny 里可写 Read(.env) 直接禁止读取该文件名,.env.example 不受影响——多个项目根同时打开时,会合并各项目里已启用的钩子
已知副作用——文档正文里出现该文件名时也可能被拦住,此时把判断收窄到路径字段——

按影响大小核对三处最容易混的地方:事件名大小写(只有 Cursor 是小写,写错静默不触发)、失败时是否放行(两家要显式 failClosed: true,TraeCode 根本没有这个字段)、配置文件位置(只有 Claude Code 写在 settings.json 里)。

示例:拦住 .env

用 preToolUse 在工具真正执行前拦一次这次调用。脚本把文件名恰好是 .env 的调用拒掉,.env.example 照常放行。文档正文里如果出现这个文件名也可能被拦住,那时再把判断收窄到路径字段。

.cursor/hooks.json

version 为 1,事件名小写 preToolUse,只有 command 与 failClosed 两项。

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "command": "python3 .cursor/hooks/block-env.py",
        "failClosed": true
      }
    ]
  }
}

.cursor/hooks/block-env.py

从标准输入读这次调用的 JSON,命中 .env 时向标准输出写拒绝 JSON,最后返回 0;没有命中就直接返回 0 放行。

#!/usr/bin/env python3
import json, os, sys

data = json.load(sys.stdin)

def mentions_env(value):
    if isinstance(value, dict):
        return any(mentions_env(v) for v in value.values())
    if isinstance(value, list):
        return any(mentions_env(v) for v in value)
    if isinstance(value, str):
        for part in value.replace("\\", "/").split():
            if os.path.basename(part.strip("\"'")) == ".env":
                return True
    return False

if mentions_env(data):
    json.dump({
        "permission": "deny",
        "user_message": "密钥只放在 .env,不能读,也不能提交。",
        "agent_message": "block-env: 拒绝访问 .env"
    }, sys.stdout, ensure_ascii=False)
sys.exit(0)

本机没有 python3 时,把 command 换成可用解释器的绝对路径。要拦第二类文件时复制一份这个脚本,只改 mentions_env 里的判断条件,其余照抄。

钩子配了却没反应的五种现象

钩子的失效大多是静默的:配置看着对,动作照做。从现象反查。

现象原因修法
钩子配了但没触发,因为事件名写成了 PreToolUse,Cursor 用的是小写的 preToolUse事件名大小写敏感,写错不报错也不触发把 hooks.json 里的事件名改成小写 preToolUse
脚本报错或超时后,危险动作照常执行failClosed 默认 false,其他非 0 退出码默认放行在配置里显式加 "failClosed": true
在 hooks.json 里写了 matcher 想只拦某类工具,没有任何效果Cursor 的钩子配置里不写 matcher,判断逻辑要放进脚本删掉配置里的 matcher,改在脚本里读标准输入的 JSON,按工具名或路径判断
把钩子配置写进了 .claude/settings.json 或另建了一个别处的 hooks.json,Cursor 不认Cursor 读的项目钩子文件是 .cursor/hooks.json把配置移到 .cursor/hooks.json,version 写 1
文档正文里提到 .env 也被拦住,助手读不了文档脚本对整个 JSON 做字符串匹配,正文里的文件名也被命中把判断收窄到路径字段,只匹配路径结尾是 .env 的项
对应知识点 这一页只是 钩子 在 Cursor 里的落地写法。通用规则见 钩子(第一层 · 上下文)。上游是 规则(说明性约定)与 子智能体,下游是 指令与配置保护。