允许规则留住了密码
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 个机密。
为什么它一路畅通
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,所以也没有密码可让允许规则存下。
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 章节。