跳到正文
原文
DEV Community · Claude Code· jidonglab·· 2天前精选AI 评分78

Claude Code 的 Read(.env) 拒绝规则为何挡不住 Bash 读取

原文标题:Claude Code Deny Rules Blocked Read(.env). Bash Read It Anyway

AI 导读

作者给 Claude Code 加了 Read(./.env) 拒绝规则,但 Claude 在 Read 被拒后改用 Bash 执行 grep DATABASE_URL .env,把生产连接串打印到了对话里。

推荐理由

作者实测发现 Read 拒绝规则挡不住 Bash 读取 .env,并给出三层防护配置,可迁移到自己的权限设置。

正文 · AI 翻译

我把 Read(./.env) 加进了拒绝列表,心里有点过意不去,然后接着干活。

一小时后,我问 Claude Code:为什么测试运行器里 DATABASE_URL 是 undefined。它试着用 Read 工具读 .env,被拒了,说“我没法直接读这个文件”,转头在 Bash 里跑了 grep DATABASE_URL .env。几周前我批准过 Bash(grep:*),因为每次搜索都要点“是”,实在点烦了。生产环境的连接字符串就这么从记录里滚了过去。

其实什么都没坏。Claude Code 的拒绝规则完全按设计工作。是我自己以为它管的是别的事。

太长不看

  • Claude Code 的拒绝规则是按工具生效的。Read(./.env) 拦得住内置的 Read 工具,但拦不住通过 Bash 跑的 cat .env、grep KEY .env 或 python -c "open('.env')"。
  • Bash 权限匹配的是命令字符串,不是文件。Bash 规则根本不知道一条命令会碰哪些文件。
  • 路径写法也容易踩坑。在权限规则里,/path 是相对于设置文件的路径,//path 是绝对路径,而 ./.env 只匹配项目根目录下的 .env。
  • 分层修复:把文件工具的通配符写对,加一个 PreToolUse Hook 拦住提到 .env 的 Bash 命令,至于真正的机密,别放在 agent 的工作目录里。

Claude Code 的拒绝规则到底拦什么?

Claude Code 的拒绝规则拦的是某个工具对某个目标的操作。一条权限规则长这样:Tool(specifier),而限定符对每个工具的含义都不一样。

  • Read(...) 和 Edit(...) 接收文件路径模式,写法跟 gitignore 一样。
  • Bash(...) 接收命令字符串模式,比如 Bash(npm run test:*)。
  • WebFetch(...) 接收域名,比如 WebFetch(domain:example.com)。

规则检查顺序是先拒绝、再询问、最后允许。所以对同一个工具,拒绝永远压过允许。这部分确实跟宣传的一样。

问题出在“同一个工具”这几个字上。一条 Read 拒绝规则只是对 Read 工具的声明,跟 Bash 工具毫无关系——而 Bash 能跑任意程序,任意程序都能打开你用户账户有权限打开的任何文件。

为什么 Claude Code 通过 Bash 读了我的 .env?

因为 Bash 是另一个工具,有自己独立的规则命名空间。Claude 跑 grep DATABASE_URL .env 时,Claude Code 拿这条命令字符串去比对的是你的 Bash(...) 规则,你的 Read(./.env) 规则根本不会被查。

我坐下来,把 agent 可能读文件的各种无聊方式都试了一遍,全在同一个项目里,Read(./.env) 已拒绝,另外配了几条常见的 Bash 允许规则:

Claude 跑了什么 受 Read(./.env) 限制吗? 结果
Read 工具读 .env 是 被拒
cat .env 否 弹提示,然后打印
grep DATABASE_URL .env 否 静默执行(我允许过 grep)
head -n 5 .env 否 弹提示,然后打印
python3 -c "print(open('.env').read())" 否 弹提示,然后打印
node -e "require('dotenv').config(); console.log(process.env)" 否 弹提示,然后打印了所有变量

六种里只有一种走了我写的规则。剩下五种全看我有没有认真读每一条 Bash 提示。到了下午第四十次审批,我根本不会认真读。

替 Claude 说句公道话,它不是偷偷摸摸。它在调试,Read 工具失败了,而 grep 是模型想回答我问题时最自然的下一个动作。绕过单个工具故障,正是你希望 agent 做的事,直到那个文件里装的是你的机密。

像 Bash(cat .env) 这样的 Bash 拒绝规则能解决吗?

不能。Bash 规则匹配的是命令文本,所以只能挡住一种写法,其他写法全漏。Bash(cat .env) 对下面这些毫无作用:

  • cat ./.env
  • cat .env.local
  • less .env、head .env、tail .env、awk 1 .env
  • while read l; do echo "$l"; done < .env
  • cd config && cat ../.env

Claude Code 官方文档自己也提醒,想用 Bash 模式去约束参数很脆弱,还举了个 curl URL 的例子,只要调换参数顺序或用个变量就能绕过。按文件名拼写去拒绝也是同样的问题。你等于是在反向列出机器上每个程序的白名单。

Claude Code 的权限规则使用哪种路径语法?

这已经是我的第二个错误了,而且它更隐蔽,因为这条规则看起来似乎没错。文件规则遵循 gitignore 风格的模式,包含四个锚点:

模式 含义
//Users/me/app/.env 从文件系统根目录开始的绝对路径
~/app/.env 相对于你的主目录
/app/.env 相对于配置文件所在位置,而非根目录
./.env 或 .env 相对于当前工作目录

如果你从终端复制一个绝对路径并粘贴为 Read(/Users/me/app/.env),那么你写下的规则实际上是以该配置文件所在位置为基准的。看起来没问题,却匹配不到任何内容。

而 ./.env 只能匹配根目录下的文件。在拥有 apps/api/.env 和 apps/web/.env.local 的单体仓库中,它完全无法覆盖。你需要的是 ** 类型的模式:

{
  "permissions": {
    "deny": [
      "Read(**/.env*)",
      "Edit(**/.env*)"
    ]
  }
}

这种模式也能拦截 .env.example,通常情况下这已经足够了。如果需要更易读,Claude 仍然可以从你的配置加载器代码中获取变量名。

我究竟该如何阻止 Claude Code 读取 .env 文件呢?

需要分三层来实现,每一层都弥补前一层留下的漏洞。

第 1 层:为文件工具设置正确的 glob 规则。如上文所示的 JSON 配置。这可以阻止整个目录树中的读取与编辑工具。

第 2 层:在 Bash 中添加 PreToolUse 钩子。钩子能在命令执行前捕获完整指令,并对其进行拦截,甚至对之前已允许的命令也有效。将此规则以 .claude/hooks/block-env.sh 和 chmod +x 的形式注册:

#!/usr/bin/env bash
# Block Bash commands that mention a .env file.
cmd=$(jq -r '.tool_input.command // ""')

if printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]_])\.env'; then
  echo "Blocked: this command touches a .env file. Ask the user for the specific value you need." >&2
  exit 2
fi
exit 0

然后将其注册到 .claude/settings.json 中:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-env.sh"
          }
        ]
      }
    ]
  }
}

该正则表达式能够匹配 .env、./.env、../.env、.env.local 和 < .env,但无法匹配 process.env,因为那里的句点前是一个字母。标准错误输出会反馈给 Claude,因此它通常不会尝试第六种变体,而是直接停止并询问我。这条消息其实是在真正发挥作用。告诉模型应该怎么做,而不仅仅是“禁止”。

第 3 层:把真正的敏感信息隔绝在外。钩子本质上是字符串匹配。无论是 cat .e""nv、经过 base64 编码的文件名,还是内部加载 dotenv 的脚本,都可能绕过它。Claude 并不打算做这些事情,但字符串匹配无法保证万无一失。因此,对于那些一旦泄露就会造成严重后果的凭据:

  • 将生产环境的值完全隔离于工作目录之外。使用密钥管理 CLI,在进程启动时注入变量;而在仓库的 .env 中仅保留开发环境的值。
  • 让代理运行在容器或虚拟机中,且绝不挂载相关文件。没有文件,进程自然也就无法读取。

我是否应该禁止像 grep 和 cat 这样的 Bash 命令?

不一定,但要清楚自己到底换来了什么。Bash(grep:*) 意味着“无需询问即可在任何地方搜索任何内容”,包括你的主目录、~/.aws/credentials 和 ~/.ssh。对于沙盒式的临时项目来说,这样的权衡还算合理;但在生产环境的根目录下存放着 .env 的仓库里,这就不太合适了。

我目前的配置是:宽泛的 Bash 权限仅限于一次性使用的临时仓库。而在正式项目中,我会启用钩子,将真实敏感信息存放在树外,并且确实会阅读那些涉及 dotfile 的提示。

Claude Code 的拒绝规则真的能保护我的 .env 文件吗?

部分有效。像 Read(**/.env*) 这样的 Claude Code 拒绝规则可以阻止内置的读取和编辑工具,但却无法阻止 Bash,因为 Bash 的权限只基于命令字符串,根本看不到某个命令具体打开了哪些文件。无论你批准或预先允许的 cat、grep、head 或 python 命令,都有可能继续读取该文件。此外,还要注意你的路径锚点:/path 是相对于配置文件的,而 //path 则是绝对路径。要想真正保护敏感信息,还需结合针对文件工具的 ** 全局拒绝规则、能够拦截提及 .env 的 Bash 命令的 PreToolUse 钩子,以及对于特别敏感的内容,干脆把文件完全排除在代理的工作目录之外。


来源:DEV Community · Claude Code · dev.to