Insights·2026-09-13

Claude Code·Codex 值得信任吗 —— 阻止 .env 值被打印到屏幕的钩子

Claude Code 和 Codex 以用户账户的权限执行命令,因此也能读取 .env 文件,而命令结果会混入对话,被发送到模型公司的服务器。AI 自行遮盖值只是 AI 遵守的承诺,而不是强制机制。所以不靠信任,而是设置两层防护。第一,在 Claude Code 的设置中加入禁止读取 .env 的规则。第二,给 Claude Code 和 Codex 同时挂上钩子,让脚本在命令执行前先检查,只拦截把值输出到屏幕的命令。像脚本自行输出值这样拦不住的路径仍然存在,但可以先堵住最常见的途径。

承接前文.gitignore 和 .env 是什么,为什么必须在第一次提交之前就建好?
Claude Code·Codex가 .env 값을 화면에 못 찍게 막는 두 겹 — source .env && python3 run.py는 통과하고, cat .env·echo $SLACK_TOKEN·printenv는 읽기 금지 규칙과 훅에 막히는 터미널 도식

AI 读取的值会去往哪里

.env 是把 API 密钥和密码集中存放在一处的文件。可以把 API 密钥理解为程序进入外部服务时出示的通行证。为什么要把这个文件与代码分开存放,前一篇文章已经讲过。本文讨论接下来的问题:如果我的 AI 编程工具打开了这个文件,会发生什么。

Claude Code 和 Codex 在我的电脑上以我的账户权限执行命令。这意味着我能打开的文件,AI 也能打开。而且 AI 必须读取命令结果才能决定下一步行动,因此结果会进入对话,并被发送到运行模型的公司的服务器。Anthropic 的 Claude Code 数据使用文档也写明,用户提示和模型输出会全部通过网络传输。如果 AI 把 .env 打印到屏幕上,密码就会成为那段对话的一部分。

发送之后的保存期限因账户类型和设置而异。按同一文档,个人套餐(Free·Pro·Max)如果允许将数据用于改进模型则为 5 年,不允许则为 30 天;团队、企业和 API 账户默认为 30 天。该设置可随时在 claude.ai 的隐私设置中更改。Codex 同样需要把命令结果发送给模型才能运行,结构相同,其保存标准需要另行查看 OpenAI 方面的政策。

问题也不只在服务器。为了能继续被中断的对话,Claude Code 会把对话记录以未加密的纯文本形式保存在我电脑的 ~/.claude/projects/ 文件夹中,默认保存 30 天。只要在屏幕上打印过一次的密码,也会留在那个文件里。

读取和看到是两回事

但这并不意味着可以不让 AI 使用 .env。程序必须读取密钥才能运行。这里要区分两件事:程序读取值,和 AI 看到值。

AI 执行 `source .env && python3 run.py` 时,打开 .env 的是 run.py 这个程序。密码只从我的电脑发往外部服务,返回给 AI 的只是程序打印到屏幕上的 "完成" 之类的输出。像 `curl -H "Authorization: Bearer $SLACK_TOKEN"` 这样命令中只写变量名的情况也一样。把变量名替换为实际值的工作由我电脑上的 shell 完成。shell 是从终端(即以文字输入命令的窗口)接收命令并执行的程序。

泄露发生在 AI 执行把值输出到屏幕的命令时。比如用 `cat .env` 原样打印文件内容,用 `echo $SLACK_TOKEN` 打印变量的值(变量是贴上名称标签、用来存放值的格子),或者执行列出全部已存储值的 `printenv`。需要拦截的不是全部文件访问,而是这种输出。

值的去向与 AI 看到的内容
.env ──> python3 run.py ──> 外部服务      (值的去向)
                 │
                 └──> 屏幕输出 "完成" ──> AI ──> 模型服务器   (AI 看到的)

遮盖是承诺,不是机制

第一次提出这个问题的那天,Claude 已经在遮盖值了。读取文件时,它附加了一条把值替换为 <set> 的命令,只显示密钥名称。这是好习惯,但这是 AI 自己遵守的规则。

规则会在两种情况下被打破。一种是 AI 的失误。在查找故障原因的调试过程中,它可能为了确认值而原样打印出来。另一种是提示注入。这是指藏在 AI 读取的网页或文档中的指令欺骗 AI,让它做出原本不会做的行为的攻击。这两种情况都无法靠 AI 的善意来防止。

所以本文的答案是:与其纠结是否相信 Claude 和 Codex 的意图,不如先建立一个无论意图如何值都出不去的结构。这就像不把保险柜密码告诉员工,只给他们打开保险柜的权限。

第 1 步:Claude Code 的 .env 读取禁止规则

最简单的第一层是 Claude Code 官方提供的权限规则。在主文件夹的 .claude/settings.json 文件中写入以下内容。settings.json 是决定 Claude Code 行为的设置文件。如果文件已经存在,只需在 permissions 中的 deny 列表里加上两行。

根据官方文档,这条规则不仅适用于 Claude 的文件读取工具,也适用于 shell 中使用的 cat·head·tail·sed 等文件命令,以及 `< file` 这样的输入重定向。`//**/` 表示电脑上任何位置的 .env 都在范围内。`.env.*` 这一行会拦截 .env.local 这类文件,用于共享的示例文件 .env.example 也会一并被拦截。

文档也写明了局限。它不适用于不写文件名、在整个文件夹中搜索文字的 `grep -r`,也不适用于用 Python、Node(两者都是编程语言)编写的脚本自行打开文件的情况。文档建议,如需在操作系统层面阻止所有程序的访问,应开启沙箱。沙箱是让 AI 执行的命令只在划定范围内运行的隔离机制。此外,这条规则是 Claude Code 的设置,不适用于 Codex。

~/.claude/settings.json
{
  "permissions": {
    "deny": [
      "Read(//**/.env)",
      "Read(//**/.env.*)"
    ]
  }
}

第 2 步:钩子 —— 命令执行前的检查站

钩子是让 AI 工具在特定时刻自动调用我的脚本的连接点。其中 PreToolUse 在 AI 执行命令或工具的前一刻被调用。脚本接收 AI 准备执行的内容,如果需要拦截,就返回拒绝信号。这样命令不会被执行,拒绝理由会传达给 AI。Claude Code 和 Codex 的拒绝信号格式相同,所以一个脚本可以同时挂在两者上。

下面是一个简短示例,只拦截第 1 步规则漏掉的情况中最常见的两种:单独执行会整体列出已存储值的 env·printenv,以及用 echo 或 printf 打印名称中含有 TOKEN·SECRET·PASSWORD·_PW·_KEY 的变量的命令。保存为 ~/.claude/hooks/block-env-print.py。Mac 上需要安装 python3。

~/.claude/hooks/block-env-print.py
import json, re, sys

cmd = json.load(sys.stdin).get("tool_input", {}).get("command", "")
secret = r"\$\{?[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|_PW|_KEY)[A-Z0-9_]*"
if re.fullmatch(r"\s*(env|printenv|export -p)\s*", cmd) or re.search(r"(echo|printf)[^|;&]*" + secret, cmd):
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "已阻止:该命令会把机密值打印到屏幕。请只使用该值,不要输出。",
    }}, ensure_ascii=False))

如何把钩子挂到 Claude Code 和 Codex 上

Claude Code 在同一个 settings.json 中添加 hooks 项。matcher 是选择在哪个工具上设检查站的字段,Bash 是执行 shell 命令的工具。与第 1 步的 permissions 并列放在同一个文件里。

Codex 在 ~/.codex/hooks.json 中按同样的格式写入。有一点不同:Codex 要求用户亲自确认并信任钩子的定义后才会执行。在打开 Codex、通过 /hooks 命令把新添加的项标记为信任之前,钩子会被跳过。

挂好之后测试一次。用 10 条示例命令测试这个示例,打印值的 5 条命令被拦截,只使用值、只查看字符数或与机密无关的 5 条命令被放行。其中六条列在下表中。在实际工具中,可以让 AI 执行 printenv,看看是否被拦截。

命令结果原因
printenv拦截列出全部已存储的值
echo $SLACK_TOKEN拦截把令牌值打印到屏幕
printf "%s" "$OPENAI_API_KEY"拦截把 API 密钥值打印到屏幕
source .env && python3 run.py放行程序只是使用值
curl -H "Authorization: Bearer $SLACK_TOKEN" …放行命令中只有变量名
echo ${#SLACK_TOKEN}放行打印的不是值,只是字符数
~/.claude/settings.json (hooks 项)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$HOME/.claude/hooks/block-env-print.py\""
          }
        ]
      }
    ]
  }
}
~/.codex/hooks.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$HOME/.claude/hooks/block-env-print.py\""
          }
        ]
      }
    ]
  }
}

实际挂上的钩子:拦截什么、放行什么、拦不住什么

每次修改实际钩子时运行的102个测试用例:应拦截命令39条、应放行命令51条、按工具区分的情况11条、故意放行的解析失败1条

示例只是入门。实际使用的钩子是按同一原理扩展了覆盖情况的一个 Python 脚本,于 2026 年 9 月 11 日同时挂到了 Claude Code 和 Codex 上。每次修改规则时,都会一次性运行共 102 个用例,包括应拦截的命令 39 条、应放行的命令 51 条、各工具的情况 11 个、解析失败 1 个,确认两个方向都正确。拦截时建议的替代命令也放进了放行列表,避免按提示修改后又被拦截。

解析失败的 1 个用例是有意放行的情况。脚本无法解析命令时,不会拦截,而是允许执行。这是因为拦截所有含糊命令会让工作停滞而选择的折中,也相应地是一个漏洞。

挂上之后的两天里,这个钩子拦截了我的 AI 三次。9 月 12 日,试图用 printf 打印 API 密钥变量的命令,以及在 Python 代码中打开 .env 的命令被拦截。AI 读取拒绝理由后,改成不打印值的形式重新执行。9 月 13 日,为了写这篇文章而检索过去对话记录的代码被拦截。检索词列表中只是包含了 ".env" 这几个字,并没有真正打开 .env,所以这是拦截了本不必拦截之物的误报。AI 把代码移到脚本文件中执行。最后这一幕恰好展示了钩子的局限:只看一行命令,无法知道脚本文件内部在做什么。下表中的 cat·grep·sed·jq·cut 都是把文件内容显示到屏幕上或只筛选出其中一部分的命令。

类别示例
拦截用 cat·head·grep·sed·jq 输出 .env 内容,用文件读取工具打开 .env,单独执行 env·printenv,echo 机密名称的变量,Python·Node 单行代码打开 .env,不带排除条件在整个文件夹中 grep 机密名称
放行source .env 后执行程序,遮盖值的 sed,只统计行数的 grep -c,只提取密钥名称的 cut·jq keys,查看文件大小·修改日期,把结果保存到文件
拦不住脚本文件自行输出值,set -x 或 curl -v 这类让命令详细打印过程的选项,把文件复制为其他名称后再读取,解析失败时放行,Codex 的网络搜索这类不经过钩子的工具

即使不是 100% 也应先挂上的理由,以及下一步

钩子只把命令当作文字来看。因此拦截范围越大,误报越多,也无法彻底阻止试图绕过的 AI。尽管如此,事故大多源于习惯而非恶意。调试时随手打印的 cat .env、为了确认设置而执行的 printenv 就是这种习惯,而拦住这两者只需要两行规则和一个简短的脚本。

下一步有三件事。第一,如果已经在对话中打印过值,就重新申请该密钥。更换密钥比从对话记录中删除更可靠。第二,为每个项目单独准备只包含该项目所需密钥的 .env。如果所有项目都加载同一个装有几十个密钥的文件,一次失误就会让那几十个密钥同时泄露。第三,检查一次所用套餐的数据设置。

今天要做的只有一件事:在 ~/.claude/settings.json 中加入两行 .env 读取禁止规则,然后请 Claude Code "执行 cat .env",看看是否被拦截。