← 返回所有文章
发布于 · 作者 Renaud Deraison

允许规则留住了密码

11 月 6 日,Apiiro 指出 Claude Code 的“Yes, and don't ask again”按钮会把批准的命令连同其中的机密存进 .claude/settings.local.json,并发现 1,122 个公开 GitHub 仓库把这个文件连同看似有效的密码、令牌和密钥一起提交了。两款常用的机密扫描器漏掉了其中 215 个。在 Bromure Agentic Coding 中,代理能打出的每一个机密都是替身,所以保存下来的规则里只有一个在哪里都用不了的值,而每一次 push 都会在 Mac 上等你回应。

你点下“Yes, and don't ask again”,是为了不再看到同一个提示。Claude Code 为了下次认出它,会把整条命令连同密码,存进你项目里的一个文件。

11 月 6 日,Apiiro 发布了关于编码代理通过“don't ask again”泄露机密的研究。The Hacker News 第二天早上做了报道。

机制很平常。当 Claude Code 请求运行一条 shell 命令,而你选了 Yes, and don't ask again,它就会在项目的 .claude/settings.local.json 里写入一条允许规则。有时规则是 npm test * 这样的模式。有时是一字不差的命令本身,而如果那条命令在 PGPASSWORD=、sshpass -p 或 mysql -p 后面带着密码,这个密码现在就躺在你代码旁边的 JSON 文件里。

Apiiro 在公开的 GitHub 上搜索这个文件,找到:

  • 103,617 个仓库中有 104,185 份副本;
  • 1,122 份副本至少含有一个机密,总计 1,379 个不同的机密;
  • 379 组数据库密码、43 组公开服务器的 SSH 登录、42 个 GitHub、GitLab 和 npm 令牌,以及 23 个 AWS 密钥;
  • 属于 63 个组织的机密,其中包括一家数据库厂商具有管理员权限的云密钥,以及一个医疗记录数据库的所有者密码。

Apiiro 检查时,这些文件全都还在默认分支上。在一半的仓库里,它们已经待了至少 217 天。较早的提交在 509 个仓库中还藏着另外 734 个机密,74 个仓库的 211 个 fork 也带着副本。把共享的 .claude/settings.json、历史记录和保存下来的代理聊天记录都算进去,Apiiro 在 1,994 个仓库中共数到 2,539 个机密。

一次点击,四步走到公开的机密批准提示PGPASSWORD=s3cr3t psql-h db.example.com …"Yes, and don't ask again"保存允许规则.claude/settings.local.json整条命令,明文保存已提交539 / 1,105追溯的仓库中,由Claude 共同撰写的提交改动公开在 GitHub1,122份含机密副本;一半已存在 217 天以上删掉文件,机密不会跟着消失旧提交中另有 734 个机密,分布在 509 个仓库;74 个仓库的 211 个 fork 带着相同的值
密码如何从一个批准提示走到公开仓库。允许规则保存一字不差的命令。代理提交项目文件夹时,文件也跟着上去,而默认分支清理干净之后,每个 fork 和每个旧提交仍保留一份副本。

为什么它一路畅通

Claude Code 确实试过把这个文件挡在 git 之外。它第一次写入 settings.local.json 时,会把文件加进你的全局 git 排除设置。但这条规则只存在于一台电脑上。同事的笔记本电脑、CI 运行器或新克隆的仓库都没有它,对已经提交的文件也毫无作用。Claude Code 从不把这个文件加进项目自己的 .gitignore。在 Apiiro 比较的 16 款编码代理中,Claude Code 是唯一一款被确认默认会把完整的批准命令存进项目文件的。

机密扫描器也好不到哪里去。Apiiro 用 gitleaks 和 TruffleHog 扫描了每一份副本。在 1,122 个文件中,gitleaks 标出 564 个,TruffleHog 标出 643 个,有 215 个两者都没抓到。这 215 个大多是写在命令里的密码。扫描器找的是 AKIA… 或 ghp_… 这类已知的密钥格式,而 sshpass -p hunter2 哪一种都不符合。

用户一年多前就报告过这个问题。2025 年 8 月的 issue anthropics/claude-code#5544 指出,Claude Code 会把含有 API 密钥的批准命令原封不动地存进这个文件。它在 60 天没有动静后被机器人以“not planned”关闭。另外四份相关报告也遇上同一个机器人,最近的一份就在 The Hacker News 文章的前一天。

机密在命令里,是因为它在代理手上

这 1,379 个机密,每一个都是代理打得出来的值。可能是开发者贴进了聊天,或留在 .env 文件里,或是代理从 ~/.aws/credentials 或 ~/.pgpass 读出来,直接嵌进它想运行的命令。允许规则只是把代理本来就有的东西记了下来。

Apiiro 对未来的建议也指向同一个方向:让机密远离命令,改为引用机密管理工具中的变量,让保存下来的规则只含 $DB_URL。前提是每位开发者和每个代理每次都照做。一个想让失败命令跑起来的代理,会把变量展开。Bromure 走的是另一条路:让真正的值完全不进入代理的环境。

同样一次点击,在 Bromure Agentic Coding 里

Bromure Agentic Coding 在你 Mac 上的 Ubuntu 虚拟机里运行 Claude Code、Codex、Grok、Kimi 和 omp。代理通过我们所说的线上边界获得凭据:在虚拟机里,每一个机密都是替身,真正的值加密存放在 Mac 上,主密钥在 macOS 钥匙串里。Mac 上的代理服务器在线路上把替身换成真正的值,而且只在发往该凭据所属主机的请求上这么做。

所以当代理请求运行一条带着令牌的命令,而你选了 Yes, and don't ask again,Claude Code 照样把命令存下来。只是其中的令牌是替身。

代理能打出的每一个机密都是替身

替身保留工具预期的格式,所以项目里什么都不用改:

Apiiro 发现泄露的代理在 Bromure 机器里拿到的
GitHub 和 GitLab 令牌ghp_ + 36 个字符、glpat- + 20 个,放在 GH_TOKEN、GITLAB_TOKEN 和 ~/.git-credentials
npm 令牌NPM_TOKEN 中的 brm_… 值,来自限定 registry.npmjs.org 的 Other API key 条目
AWS 密钥真实的访问密钥 ID 配上假的秘密密钥;Mac 用真实的密钥重新签名每个请求
数据库密码你指定的变量中的 brm-db-… 值,适用于 ClickHouse、Mongo Data API 和 Elasticsearch
SSH 登录完全没有密码:只有由留在 Mac 上的密钥生成的签名

以代理可能对 ClickHouse 端点运行的一条命令为例:

curl -u default:brm-db-7Qk2…Xw9 \
  'https://analytics.example.com:8443/?query=SELECT+count()+FROM+events'

把它永久批准,允许规则里存的就是 brm-db-7Qk2…Xw9。代理服务器会在发往 analytics.example.com 的请求中替换替身,Basic-auth 请求头和请求体都会换,因为数据库机密常常夹带在 SQL 和 JSON 里。换到其他任何地方,这串字符都毫无用处。

至于 AWS,机密根本不会进到虚拟机。代理的 ~/.aws/config 指向一个辅助程序,它交出真实的访问密钥 ID 和一个 40 个字符的假秘密。AWS CLI、boto3 和 terraform 用假的签名,Mac 则在请求发出前去掉这个签名,再用真实的秘密重新签名。真实的秘密密钥从不存在于虚拟机中,所以代理写出的任何命令都不可能含有它。

Apiiro 统计中的 43 组服务器登录属于 SSH。Bromure 机器的 SSH_AUTH_SOCK 是通往 Mac 的桥接:虚拟机可以请求签名,但私钥的字节永远不会跨过来。由 YubiKey、Secretive 或 1Password 保护的密钥可以逐一提供,触摸确认会出现在你的 Mac 上。代理从不需要 sshpass -p,所以也没有密码可让允许规则存下。

机器虚拟机批准的命令GH_TOKEN=ghp_Xk3…a7Q gh api …"Yes, and don't ask again"settings.local.json存的是替身,只有通过这台 Mac 的代理服务器才有效git push提交里包含这个文件你的 Mac代理服务器在线路上替换替身 → 真令牌,只限发往 github.com 的请求真正的值,钥匙串加密令牌、AWS 机密、SSH 密钥:从不以任何形式进入虚拟机Prompt before writepush 等你回应互联网api.github.com收到真正的令牌,命令照常运行有人复制了文件从仓库、fork 或旧提交取得并试用令牌:401 Bad credentials
在 Bromure 机器里的同一次点击。允许规则依然保存命令,但其中的令牌是替身。会把它公开出去的 push 要等 Mac 上的对话框,而从 GitHub 复制走的替身在哪里都会失败,因为只有你 Mac 上的代理服务器能把它变回真正的值。

GitHub 上的替身什么也打不开

假设文件真的到了公开仓库、一个 fork 和十几个旧提交里。把它抓走的人拿到的是 ghp_ 加上 GitHub 从未签发过的 36 个字符。从他自己的笔记本电脑发出,认证就会失败。只有你 Mac 上的代理服务器知道它对应哪个真正的令牌,而那台代理服务器只服务你机器的虚拟机。

如果替身哪天从机器内部被发往错误的主机,代理服务器会把它当成外泄企图。它以 HTTP 451 拦下请求、暂停虚拟机,并弹出标题为 This environment may have been compromised 的警告,列出该凭据、它所属的主机,以及它被发往的主机。因此每个替身不只是占位符,也是一根绊线。

轮换只需在一处完成

Apiiro 的第一条指示是:在清理任何东西之前,先轮换文件里曾经出现过的每一组密码、令牌和密钥。当真正的机密散落在 .env 文件、shell 历史记录和代理配置里,这就成了一场寻宝。在 Bromure 中,真正的值只放在一处:Mac 上机器的 Credentials 面板。把新令牌贴进去,虚拟机里的替身保持不变,机器内的允许规则、脚本和配置都能继续工作。

Other API key 也可以是一个 1Password 引用,例如 op://Engineering/npm/token。Bromure 在 Mac 上解析它,并每 2 分钟重新解析一次,所以在 1Password 里轮换的密钥几分钟内就会生效,无需重启,虚拟机里也没有东西要改。

把机密移进这个面板只要一步。Import env file… 会读取现有的 .env 或 ~/.bashrc,以遮蔽的值列出变量,再把它们转成凭据。项目文件夹从此不再需要那个代理用来读密码的 .env 文件。

push 会等你

在 Apiiro 追溯的仓库中,有 539 个是由一个把 Claude 列为共同作者的提交改动了这个文件。代理在完成任务的过程中会提交并 push。

在新的 Bromure 机器上,Guardrails 面板里的每个代码托管服务一开始都是 Prompt before write。读取照常通过。通过 HTTPS 的 git push 会停在 Mac 上一个显示该操作的对话框,在你回应之前什么都不会发出。这项策略在代理服务器中执行,所以虚拟机里没有任何东西能把它关掉。

会读那一行的审查者

没有任何扫描器抓到的 215 个机密,都是命令里的密码。一个模型在已提交的 JSON 文件中读到 PGPASSWORD=… psql -h db.example.com,就知道自己在看什么。

Bromure 的代码审查会用你某台机器上的代理监视一个 GitHub 仓库,时机可选 Every new commit、Every new pull request 或每周计划,Bromure 也会跨次扫描去除重复的发现。对应 Apiiro“搜索整个组织,包括历史记录”这一步,Run Baseline Now 会对整个仓库启动一次性审查。GitHub 由 Mac 轮询,审查代理用的也是和其他代理一样的替身。

对话记录里留下的也是替身

Apiiro 的统计包含保存下来的代理聊天记录。虚拟机里 Claude Code 自己的会话文件记录了代理看到的内容,而在 Bromure 机器里,那就是替身。Mac 上 Bromure 的追踪记录把令牌替换保存成像 sk-a…f9q3 这样的简短预览,真正机密的字节从不进入任何追踪记录。如果流量中还是出现了看似真实的机密,比如有人手动把它贴进虚拟机,追踪记录会把它标为可能的泄露,bromure-cli trace leaks 则会全部列出。在 Guardrails 中开启 Only allow credentials Bromure injects,任何携带非 Bromure 注入之凭据的请求都会被拦下,并触发入侵警告。

每条命令里都是替身

虚拟机中的令牌、数据库机密和 AWS 密钥都是替身。允许规则存下的值,只有通过你的 Mac 才用得了。

SSH 不需要密码

虚拟机向 Mac 请求签名。私钥永远不会跨过来,也从不需要 sshpass -p。

push 在 Mac 上等待

新机器默认就是 Prompt before write。push 会等你回应,由代理服务器强制执行。

轮换一次就好

真正的值放在一个面板里,或在 1Password 中。在那里改,每个替身都照常工作。

这周该做的事

摘自 Apiiro 的清单,适用于所有使用编码代理的团队:

  • 在你的 GitHub 组织中搜索 .claude/settings.local.json 和 .claude/settings.json,包括历史记录。
  • 轮换这些文件曾经含有的每一个机密,然后再清理。
  • 运行 git rm --cached .claude/settings.local.json,并把它加进项目的 .gitignore 和组织的模板。
  • 在 pre-commit 和 CI 扫描中,把 .claude/、.cursor/、.gemini/ 和 .specstory/ 当成 .env 文件对待,并确认扫描器能抓到命令里的密码。

接着,把真正的机密移出代理够得着的范围。在 Bromure 机器里,不论开发者那天有没有记得 Apiiro 的规则,下一次“don't ask again”存下的都是替身。

关于代理把机密复制进历史记录和日志的更广泛模式,请见令牌留在了对话记录里。想了解替换的详细工作方式,请阅读手册的 Credentials 章节。