一次"以后别再问我",把密码写进了全世界的 GitHub
2026 年 11 月 5 日,安全公司 Apiiro 发了一份研究。核心数字是:GitHub 上有 1,122 个公开文件,各自至少存着一个真实凭据,合计 1,379 个。
这些文件的路径是 `.claude/settings.local.json`。
不是泄露事故,不是服务器被攻破。是一行一行被开发者亲手点出来的。
机制:一次点击,一条明文
Claude Code 默认会在执行 shell 命令前询问。三个选项:允许、允许并别再问、拒绝。
选"允许并别再问",它会把这个审批变成一条常驻规则,写进项目里的 `.claude/settings.local.json`,以后同类命令不再询问。
问题在于,这条规则保存的是命令原文。
当提示框给出的是一条完整命令,而不是通配模式时,命令里任何密码、Token 都会被原样存下来,明文,不加密。
Apiiro 给出的示意大概是这样:
psql postgresql://user:真实密码@prod-db.example.com/app
一条连数据库的命令,一次"别再问我",密码就落进了项目目录里的一个 JSON 文件。
这一点 Anthropic 自己的权限文档是写着"审批会保存在本地设置文件"的。它没写的是:保存的内容包含凭据本身。
保护机制差一步
Claude Code 不是完全没做防护。它在首次于某个 git 仓库内写入这个文件时,会把该文件加进用户的全局 git excludes。
这条保护有两个致命边界。
第一,它是"全局"的,但只对一台机器生效。
开发者 A 在自己电脑上让 Claude Code 写过这个文件,于是 A 的 `~/.config/git/ignore` 里有了这条规则,A 的所有仓库都忽略它。
但开发者 B clone 了同一个仓库,第一次点"别再问我"时,B 的机器上没有那条 excludes(除非 B 也触发过一次),文件就会正常进入 git 的跟踪范围。B 一提交、一推送,凭据就上去了。
第二,它加的是全局 excludes,不是项目自己的 `.gitignore`。
项目级的 `.gitignore` 会跟着仓库走,谁 clone 谁生效。全局 excludes 不会。这两者的差别,正好是"只保护创建者的电脑"和"保护所有协作者"的差别。
还有一个更隐蔽的情况:如果一个目录本来是普通文件夹,后来才 `git init` 成仓库,那这个文件早就在里面了,Claude Code 的首次写入钩子根本不会触发。
扫描器看不见的部分
这是全篇最有价值的一段。
Apiiro 用两个业界最常用的密钥扫描器跑了一遍全部 104,185 个文件:
gitleaks 命中 564 / 1,122
TruffleHog 命中 643 / 1,122
两者都漏 215 个(19.2%)
接近两成的凭据,两个主流扫描器都扫不出来。
原因不在工具不好,在凭据的形状。
密钥扫描器的逻辑是匹配已知格式——`sk-` 开头的 OpenAI key、`AKIA` 开头的 AWS key、`ghp_` 开头的 GitHub token。这些都有固定前缀,正则一抓一个准。
但密码没有格式。
PGPASSWORD=MyP@ssw0rd2026
sshpass -p hunter2 ssh root@1.2.3.4
mysql -u root -pTr0ub4dor
在扫描器眼里,这些就是一个普通命令行里的一串普通字符。它长得不像密钥,因为密码本来就不该长成密钥的样子。
GitHub 自带的 Secret Scanning 同理。
这个盲区值得所有做安全的人记住:你的扫描器认识的是"格式",不是"语义"。
数字背后的图景
几个分层的统计:
| 范围 | 数量 |
|---|---|
| 公开的 `.claude/settings.local.json` | 104,185 个,分布在 103,617 个仓库 |
| 含真实凭据的当前文件 | 1,122 个(1.1%) |
| 这些文件里的不同凭据 | 1,379 个 |
| 加上 git 历史、共享 settings.json、agent 对话记录 | 2,539 个凭据 / 1,994 仓库 / 63 个组织 |
凭据类型:
379 数据库密码
122 Supabase service-role key(服务角色密钥,可绕过 RLS)
103 Supabase 账户 token(Apiiro 称可控制账户下所有项目)
43 SSH 登录(部分含 root 密码)
42 GitHub / GitLab / npm token
23 AWS key
6 私钥
超过三分之一(501 个,36.3%)直接能开数据库。
按 Apiiro 的分类,1,379 个里 636 个是可识别格式的 provider key,743 个是"密码或面向公网服务的 token"。
最严重的个案包括:某数据库厂商自己的管理员级云密钥、某医疗记录数据库的 owner 密码、一个生产环境的 Azure PostgreSQL 密码、一个 Square 生产 token。
时间维度更值得看
- 半数(约 561 个)仓库,这个文件在默认分支上待了至少 217 天
- 118 个仓库超过一年
- Apiiro 推算这些文件首次提交的中位数约在 2026 年 3 月 30 日
也就是说,这不是最近才出现的新问题,是问题存在了大半年,只是最近才有人去数。
清理也没用:
509 个仓库的旧版本里还留着 734 个凭据(当前文件已删,历史里还在)
211 个 fork(来自 74 个原始仓库)携带了完全相同的凭据
删文件不等于删凭据。 git 历史会留着,fork 会留着,clone 出去的副本谁也数不清。
一个不太舒服的细节
Apiiro 追踪了 1,105 个仓库的提交记录,其中 539 个(48.8%) 有"Claude 作为 co-author"的提交修改过这个设置文件。
这不能证明是 Claude 主动提交的——co-author 标记只说明那次提交里 Claude 参与了内容生成,推不推、提交不提交,还是人决定的。
但它确实说明一件事:在很多场景里,是这个工具自己把这个文件带进了提交范围。
Anthropic 的反应
这部分是文章里最该被记住的。
最早的相关 bug report 提交于 2025 年 8 月 11 日,明确写道"已批准的包含 API key 和 token 的命令被逐字保存进 settings.local.json"。
2026 年 1 月 3 日,一个自动化 bot 在 60 天无活动后把它标记为 "not planned" 关闭。
Apiiro 引用的 4 个相关报告,全部被同一个 bot 关闭,最近一次是 11 月 6 日——也就是研究公开的第二天。没有任何一个得到过 Anthropic 的人工回复。
截至发稿,Anthropic 未对这项研究公开回应。
这不是个技术问题,是个流程问题。一个被报告了 15 个月、有明确定位、有复现步骤的问题,栽在了自动关闭机制上。
边界:这份研究没说什么
必须说清楚,否则就是拿数据编故事。
① 没有验证凭据是否有效。
Apiiro 自己的定义是"匹配某个 provider 的格式,或指向一个公网主机"——是分类,不是测试。他们没有用任何一个凭据去连,也没有联系任何主机。
所以"1,379 个凭据"≠"1,379 个能用的凭据"。
唯一的实证:一家被通知的公司在研究中确认,自己的密钥当时确实是活的。
② 没有发现被利用的证据。
研究者明确表示,不知道是否有人已经用过这些凭据。
③ 没有覆盖全部场景。
只在 Opus 5.5 和 Fable 5.1 两个模型上复现,没有测所有 Claude Code 版本、所有操作系统。
④ GitHub 上的可能只是小头。
Apiiro 自己认为,留在开发者本机、从未提交的文件数量更大。这类文件还可能被打进 npm 包、Docker 镜像、zip 里,或者躺在同步盘上。
该怎么做
按优先级排,顺序不能反。
第一,先轮换,再清理。
这是 Apiiro 反复强调的:删除文件不会撤回已经发生的事。凭据一旦以明文形式进过公开仓库,就必须当它已经泄露。
顺序是:
1. 轮换 → 2. 清理 → 3. 加防护
反过来做没有意义。
第二,查全,包括历史。
# 当前是否被跟踪
git ls-files | grep -E '\.claude/settings(\.local)?\.json'
# 历史里是否有过
git log --all --full-history -- '*claude/settings*.json'
# 在整个 GitHub 组织范围内查
gh search code --owner <你的组织> --filename settings.local.json
第三,停止跟踪。
git rm --cached .claude/settings.local.json
echo '.claude/settings.local.json' >> .gitignore
注意是项目自己的 `.gitignore`,不是 `~/.config/git/ignore`。前者跟着仓库走,后者只保护你自己。
第四,改掉用命令传密码的习惯。
# 不要这样
psql postgresql://user:真实密码@host/db
# 这样
psql "$DATABASE_URL"
规则里存的是 `$DATABASE_URL` 这个字面量,不是密码。
第五,审批模式,不审批单条命令。
好:Bash(npm run:*)
坏:Bash(npm run build --token=abc123)
第六,把 agent 目录加进扫描范围。
`.claude/`、`.cursor/`、`.gemini/` 这些目录,以及 agent 的记忆文件、对话记录,都该进 pre-commit 和 CI 的扫描列表,包括历史。
而且要确认一件事:你的扫描器能不能识别命令里内嵌的密码? 如果它只认 `AKIA`、`ghp_`、`sk-` 这类前缀,那 19.2% 的盲区你也有。
我的判断
这件事真正的教训,不在"Claude Code 有 bug"。
在于"便捷"和"留痕"这对矛盾,被一行默认值处理掉了。
"以后别再问我"是个好设计——没人想为同一条命令点一百次。但它的实现选择是:把整个命令字符串当成规则保存。
这是最省事的实现。也是最容易把凭据写成明文的实现。
如果实现成"只保存命令的第一个 token"(`psql`),或者"保存命令但把疑似凭据的部分脱敏",这个洞根本不会存在。
第二个教训关于保护边界。 全局 git excludes 是一个"看起来做了保护"的设计——它确实生效,但只在创建者那一台机器上生效。这种保护比没有保护更危险,因为它给了人"已经处理过了"的错觉。
好的防护应该跟着代码走:`.gitignore` 在仓库里,clone 就带着;pre-commit hook 在仓库里,团队共享;CI 扫描在流水线里,绕不过去。
第三个教训,也是我觉得最值得写下来的: 一个 2025 年 8 月就被准确报告的问题,在 2026 年 1 月被 bot 以 "not planned" 关掉,又过了 10 个月才被第三方安全公司用 10 万个文件的样本重新挖出来。
自动化能处理噪音,但处理不了"看起来像噪音的重要信号"。这套流程省下的人力,最后以一种更贵的方式还了回去。
信息来源:Apiiro 研究报告(2026-11-05,官网当时不可达,经 The Hacker News、Cybersecurity News、Breach Signal、letsdatascience 等多方转述交叉核对)、Anthropic Claude Code 权限与设置文档
数据说明:文中所有数字均来自上述来源的公开转述。Apiiro 未验证凭据有效性,亦未发现被利用的证据;本文同样没有做任何验证性访问。