本页判断标准

在对话里让助手把 .env 的内容贴出来,它能不能被挡住?

答「能贴出来」,这条钩子等于没设。TraeCode 没有 failClosed,脚本崩了、超时了都算放行。先单独跑一次脚本做对照,再去查 matcher。

事件、拒绝方式与超时

钩子只有两个出口:拦住,或放行。写的配置都围绕这两件事:脚本怎么表达拦住,以及脚本出问题时算哪一边。

项TraeCode 的写法写错的后果
项目配置.trae/hooks.json放到别处,当前项目不生效
用户级配置~/.trae-cn/hooks.json,本机所有项目共用小组约定只在自己电脑上
version目前只用 1——
事件名PreToolUse,在工具执行前触发大小写写错不会报错,只是不触发
matcher是正则,匹配工具名:RunCommand、Read、Write、Edit写成 Bash 之类别的名字,钩子永不触发
配置形状matcher 配 hooks: [{type, command, timeout}] 两层层级放错,钩子不执行
type只用 command——
输入脚本从标准输入读 JSON读不到输入还继续往下判断,容易崩
拒绝方式退出码 0 并在标准输出输出 permissionDecision 为 deny 的 JSON只 print("deny") 不够,要按 JSON 结构输出
另一种拒绝退出码 2,把原因写在标准错误,交给模型——
其他退出码不拦截脚本抛异常退出码为 1,等于放行
超时timeout 不写默认 30 秒;超时等于没拦住脚本里做网络请求,超时后形同虚设

字段不用背:建一条钩子,跑一遍「怎么知道它真的在拦」的四步,实测一次就知道了。先看最后两行:TraeCode 没有 failClosed 字段,也没有「脚本失败时按拒绝处理」的开关,所以脚本必须自己不崩溃——下面示例里用 try/except 保证这一点。

钩子在三个工具里的差别

最容易混三处:文件是不是 hooks.json、事件名的大小写、脚本失败时算哪一边。最后一行是这三家里差别最大的一条。

项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 不受影响——多个项目根同时打开时,会合并各项目里已启用的钩子

写完配置,先对照「事件名」与「脚本崩溃或超时」两行自查一遍。整体对照见TraeCode 总览的七处位置总表。

示例:拦住 .env

先确认本机有 python3。文件名恰好是 .env 就拒绝,.env.example 可以读。脚本遍历输入里的所有字符串值,所以不依赖具体的字段名;解析失败时它也输出 deny,避免崩溃被当成放行。

.trae/hooks.json

{
  "version": 1,
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "RunCommand|Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .trae/hooks/block-env.py",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

.trae/hooks/block-env.py

它只有一件事要做:一旦拿不准,就让它输出 deny,绝不以异常退出。

#!/usr/bin/env python3
"""读工具调用参数,命中 .env 就拒绝。

TraeCode 没有 failClosed:脚本崩溃或超时,这条钩子就等于不存在。
所以任何一处不确定,都要走输出 deny 这条路,不要让它以异常退出。
"""
import json
import os
import sys

DENY = {
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "密钥只放在 .env,不能读,也不能提交。",
    }
}

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

def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        json.dump(DENY, sys.stdout, ensure_ascii=False)
        return 0

    if mentions_env(data):
        json.dump(DENY, sys.stdout, ensure_ascii=False)
    return 0

if __name__ == "__main__":
    sys.exit(main())

怎么知道它真的在拦

配好不等于生效。下面四步每季度做一次,十分钟能做完,验证的是「拦住」这件事到底存不存在。

第一步 · 造一次违规在对话里让助手读 .env,或让它把 .env 的内容贴出来。正常表现是它收到拒绝理由,读不到内容。
第二步 · 单独跑一次脚本做对照跑 echo '{"any": ".env"}' | python3 .trae/hooks/block-env.py,它应当输出带 "permissionDecision": "deny" 的 JSON;把 .env 换成 .env.example 再跑一次,应当没有任何输出。
第三步 · 验证没有 failClosed在脚本开头加一行 import time; time.sleep(40),再触发一次读 .env。过了 30 秒它会放行——这就是没有 failClosed 的直接后果。测完把那句删掉。
第四步 · 记录并交接把这次实验的日期与结果写进 docs/decisions/,下次换一个同学来做。

第三步最容易跳过,但它解释了这个页面开头那句话:脚本出问题,钩子就不存在。所以脚本要短、要没有网络请求、要把异常都兜住。

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

钩子的失败几乎都不报错——它只是安静地不做事。照下面五条对号最快。

现象原因修法
脚本里明明写了 deny,助手照样读 .env脚本崩了:JSON 解析失败或文件路径不存在,以非零码退出,被当成放行;TraeCode 没有 failClosed用 try/except 包住解析,异常分支也输出 deny JSON 并 exit 0;见上方第三步的对照实验
matcher 写了 Bash|Write|Edit,钩子一次都没触发TraeCode 的工具名是 RunCommand、Read、Write、Edit;matcher 按正则匹配工具名,写成 Bash 不报错,只是不触发改成 "matcher": "RunCommand|Read|Write|Edit",再用第一步的违规动作确认被拦
读 .env 有时被拦、有时放行脚本超时。TraeCode 没有 failClosed,超时等于这个钩子不存在让脚本保持轻量:不做网络请求、不扫大目录;timeout 写 30(秒)够用即可
自己电脑上拦得住,换一个同学的电脑就不拦配置写进了 ~/.trae-cn/hooks.json,那是本机用户级目录,不进仓库把配置放到项目的 .trae/hooks.json 与 .trae/hooks/block-env.py,一起提交
同时开了两个项目,另一个项目的关也被拦了TraeCode 会合并各项目里已启用的钩子把判断条件收窄到明确的路径,不要只靠「文件名叫 .env」这种通用规则去命中其他项目
对应知识点 这一页是 钩子 在 TraeCode 里的落地写法。上游是钩子(第一层 · 上下文)与规则,后者说明哪一类动作该写成钩子、哪一类约束写成规则;下游是指令与配置保护。