11 月 9 日和 10 日,GitGuardian、Ars Technica、IT之家都在讲同一件事:Claude Code 里选「Yes, and don't ask again」(中文界面是「是,且不再询问」),命令原文会写进项目的 .claude/settings.local.json。公开仓库里已经有人把带密钥的规则提交上去了。
我没有去搜别人的仓库,也没有验证任何密钥。我只在自己这台 Windows 上做了两件事:数 F:/desk_code 顶层的 27 个 git 仓库里,这个文件有没有被跟踪;再在临时目录里用一条没有密钥的假规则,把「忽略规则为什么盖不住已跟踪文件」跑出来。
27 个里有 2 个磁盘上存在这个文件。1 个被仓库自己的 .claude/ 忽略了。另 1 个,576 字节,已经被 git ls-files 跟踪,git check-ignore 返回未忽略。本机的全局 git 排除文件在,但里面没有这一行。
先说结论
| 检查 | 这台机器上的结果 |
|---|---|
| 顶层 git 仓库 | 27 个 |
磁盘上有 .claude/settings.local.json | 2 个 |
已被跟踪,且 git check-ignore 说没忽略 | 1 个,576 字节,16 条 allow |
被仓库 .gitignore 盖住 | 1 个,315 字节,4 条 allow,规则是 .claude/ |
~/.config/git/ignore | 有这个文件,6 行,没有 settings.local.json |
三条可以带走的:
- 官方文档写的是:Claude Code 第一次自己写出这个文件时,才会把
**/.claude/settings.local.json加进全局 git 排除。我这台没加过。文件是你自己建的,或者那次写入没发生,仓库侧就什么都没有。 - 已经进了索引的文件,后补
.gitignore不会让它退出。git check-ignore对已跟踪文件直接返回「未忽略」,哪怕规则其实写对了。 .claude/*和.claude/不是一回事,但在「已经跟踪」这件事上结果一样:两条都挡不住索引里的那一份,都要先git rm --cached。
背景
GitGuardian 11 月 9 日的文章(Guillaume Valadon,13 分钟)统计的是公开 GitHub 上 2025 年 1 月到 2026 年 11 月 7 日推送过的版本:.claude/settings.local.json 里 31,088 条不同的密钥,分布在 23,276 个仓库,检出时有 5,343 条还能用。同一套口径下,团队会提交的 .claude/settings.json 是 8,271 条、6,839 个仓库、1,283 条检出时有效。
IT之家 11 月 10 日转的是另一组数,来源是 Apiiro,经 Ars Technica 转述:超过 10 万份本地设置文件进了公共仓库,约 1,100 份含有机密,连同 Git 历史、共享设置和聊天记录,共 1,994 个代码库里的 2,539 个真实凭据。GitGuardian 在正文里写了,这两组数回答的不是同一个问题:Apiiro 数的是「现在还敞着的」,他们数的是「曾经被推送过、检出时有效、该轮换的」。
机制本身三边说的是一件事。官方 权限文档 写:对 Bash 命令选 “Yes, and don't ask again”,永久允许会存进 git 仓库根的 .claude/settings.local.json,规则按仓库、按命令生效。改文件的那种允许只活到会话结束,不进这个文件。设置文档 写得更具体:选了之后,批准会变成一条 allow 规则;第一次由 Claude Code 在一个还没忽略它的 git 仓库里写出这个文件时,才会往全局 git 排除里加 **/.claude/settings.local.json。排除文件是 core.excludesFile(要是绝对路径或 ~ 开头),否则是 $XDG_CONFIG_HOME/git/ignore,没设 XDG_CONFIG_HOME 就是 ~/.config/git/ignore。文档也写了:文件要是你自己建的,而 Claude Code 还没往里写过,就得自己加进 .gitignore。
GitGuardian 抽了大约 3,000 处出现位置,能定位的密钥都在 allow 规则或 env 块里,没有一条在 deny。命令类型里 curl 占一半,主要是 bearer、JWT、请求头里的 key,能验证的比例低;数据库和 SSH 只占 15% 和 1%,但能用的比例高,psql / mysql 一类在可检查的里面到一半以上,sshpass -p 是 54%。他们的原话是:量在 curl,真正有效的凭据在数据库和 SSH。
实测
环境:Windows 10,claude --version 输出 2.1.318 (Claude Code),git --version 是 git version 2.31.1.windows.1。git config --global --get core.excludesFile 退出码 1,没有单独设过。$XDG_CONFIG_HOME 是空的,所以默认排除文件是 ~/.config/git/ignore,6 行,内容是另一套工具的 *.local.md,没有 Claude Code 这一条。
交互里点「不再询问」这一步我没跑成。临时目录里起 Claude Code 需要登录和一轮来回点选,这次没有走通。写入格式用的是官方文档的说法:规则是命令字符串本身,包在 Bash(...) 里,放进 permissions.allow。下面的复现用的是假规则 Bash(echo pick-1111),没有任何密钥。
27 个仓库,只看状态,不看内容
只扫顶层带 .git 的目录,跳过 node_modules。命中 2 个文件,都在仓库根的 .claude/ 下。对这两个文件只做了结构统计:顶层键、permissions.allow 条数、每条长度、有没有 *、有没有 sk-、AKIA、xox、ghp_、-----BEGIN、以及 ://用户:密码@ 这种形状。两份都没有命中这些形状,所以下面只写条数和长度,不贴规则原文,仓库名也不写。
被忽略的那份,315 字节,只有 permissions 一个顶层键,4 条 allow,全是 Bash,长度 15、17、170、16。第 1 条带 *,第 3 条是 170 个字符的整句。git check-ignore -v 的输出是:
.gitignore:66:.claude/ .claude/settings.local.json
git ls-files 和 git status --short 都是空的。这条目录规则盖住了它。
已被跟踪的那份,576 字节,同样只有 permissions,16 条 allow,全是 Bash。13 条带 *,是前缀规则;3 条没有 *,长度 99、28、10,按文档的语法就是整句精确匹配。git ls-files 列出了 .claude/settings.local.json。git status --short 是空的,工作区和索引一致。git check-ignore -v 退出码 1,什么都不打印。
这个仓库的 .gitignore 第 2 行是:
.claude/*
同一条规则,对一个还没进索引的 .claude/foo.json,git check-ignore -v 会命中。对已经跟踪的 settings.local.json,不命中。加上 --no-index 再查一次,它又命中了:
.gitignore:2:.claude/* .claude/settings.local.json
也就是说规则其实盖得到这个路径。普通的 git check-ignore 返回「未忽略」,只是因为文件已经在索引里,git 不再拿忽略规则去处理它。这个文件第一次进仓库是 2025-07-19 那次提交,最近一次改它是 2025-10-25,那次 diff 是 8 行新增、2 行删除。.claude/ 下面被跟踪的只有这一个文件。
临时仓库:规则在,文件照样进得去
临时目录 jobs/claude-settings-local-gitignore-1111/src/probe/,三个小仓库,规则都是假的 Bash(echo pick-1111)。
先跟踪、后加忽略。没有 .gitignore 时 git add -f 把文件加进去并提交,然后再写入 .claude/*:
check-ignore (tracked):
NOT_IGNORED rc=1
ls-files:
.claude/settings.local.json
status:
工作区是干净的,文件仍在索引里。再追加一行 .claude/ 并提交,结果一样,git ls-files 还是列得出它。然后:
git rm --cached -- .claude/settings.local.json
这一步之后 git check-ignore -v 才打印 .gitignore:2:.claude/,git status --short 变成 D .claude/settings.local.json(只是索引里删了,还没提交),磁盘上的文件还在。
先有规则、再强制加入。.gitignore 已经是 .claude/* 时,git add -A 不会带上这个文件,git ls-files 里只有 .gitignore 和 README.md。换成 git add -f .claude/settings.local.json,它就进索引了,随后 git check-ignore 同样返回未忽略。这时候在旁边放一个 .claude/other.json 再 git add -A,新文件仍然被挡住:
check-ignore other:
.gitignore:1:.claude/* .claude/other.json
所以「规则写了」和「这一份已经在索引里」是两件事。规则继续保护还没进来的文件,不负责把已经进来的赶出去。
全局排除也试了。一个新建的仓库,不设 core.excludesFile、仓库里也没有 .gitignore,settings.local.json 不被忽略。把 core.excludesFile 指到一份只写了 **/.claude/settings.local.json 的临时文件之后,仓库根和子目录 sub/.claude/ 里的同名文件都被忽略。取消这个设置,回到本机默认的 ~/.config/git/ignore,AGENTS.local.md 会被第 6 行的 *.local.md 忽略,settings.local.json 不会。
这 27 个仓库的检查是只读的,没有改它们的 .gitignore,也没有对那个已跟踪文件执行 git rm --cached。临时仓库留在任务目录里,没有推到任何远端。
原文没分开的三件事
「文件在,但没进仓库」和「文件已经被跟踪」要分开查。 IT之家写的是:全局 .gitignore 换机器、或者文件夹后建仓库时会失效。这是「还没跟踪」的那条路,我这边用临时仓库验证了:默认全局排除里没有这一行,新仓库确实不会忽略它。另一条路是文件早就在索引里。这时候你把规则补进仓库 .gitignore,git status 看起来是干净的,因为改动已经在提交历史里了。只看 git status 会以为没事。
.claude/* 会给人一种「我已经忽略了」的错觉。 gitignore 里不带斜杠的 * 不跨目录,但 .claude/* 这种带斜杠的写法,匹配的是 .claude 下面任意深度,我用 .claude/sub/a.txt 试过,它被同一行命中。所以星号规则不是漏了子目录。它漏的是另一件事:对已经跟踪的文件,忽略规则不再生效。那个 576 字节的仓库正是这种,.gitignore 第 2 行看着像覆盖了整个 .claude,git check-ignore 却说没忽略。
Boris Cherny 说的「多半是人手建了再提交」和这台机器对得上,但不能当成全部。 Ars Technica 11 月 9 日引他在 Hacker News 上的回复:团队在看能做什么,Claude Code 历史上没有清过密钥,auto mode 和内置密钥扫描在做原型、未必能完全解决;文件在创建时会被 gitignore,露出去的那些多半是人手建了再提交;他更建议用 auto mode,而不是手写 allow/deny。GitGuardian 的反驳是:他们抽的 3,000 处里,密钥都坐在权限提示会写出的那种 allow 规则上,不是手敲的形状。我这台机器两种情况都有影子:全局排除是空的,说明「创建时写进全局排除」在这里没发生;已跟踪的那份从 2025 年 7 月就在索引里,比这次报道早一年多,单凭现在的文件看不出是谁执行的 git add。
还有一个文档里写了、报道里没展开的行为。设置文档说,在 Windows 上,Claude Code 不一定把这个文件放在仓库根,会改放到别的位置。我扫到的两份都在仓库根,和「放在子目录所以逃过根上的忽略」不是一回事。权限文档还写:这个文件一旦被 git 跟踪,或者 .claude 是符号链接,Claude Code 会把它当成仓库提供的配置,allow 规则要等你信任这个文件夹之后才生效。已跟踪不只是泄露问题,也会改你本机的信任行为。
GitGuardian 还量了公开 Docker Hub:.claude/settings.local.json 出现 109,284 次,跨 543 个镜像仓库、7,625 个 tag,去重后 613 条密钥、99 条有效。72% 落在 /app、/workspace、/usr/src,也就是 COPY . . 把工作区拷进镜像。COPY 看的是工作区,不是 git 索引,所以一个被 git 忽略的文件,只要 .dockerignore 没列 .claude/,照样进镜像。我这台没有用这些仓库打镜像,这条没有实测,只把范围记在这里:从索引里拿掉,盖不住已经推上去的镜像。
这台机器上先做哪一步
不要从删文件开始。GitGuardian 写的第一件事是轮换这个文件曾经装过的凭据,因为历史、fork 和镜像里还有副本。我这两份没有扫到常见密钥形状,不代表以前的提交里没有,576 字节那份从 2025-07-19 起就在历史里。
还没被跟踪的,把规则写进这个仓库的 .gitignore,不要只靠全局排除:
.claude/settings.local.json
或者直接忽略整个目录:.claude/。官方往全局排除里写的是 **/.claude/settings.local.json,这样子目录里的同名文件也会被盖住。
已经被跟踪的,补忽略之后还要把它从索引拿掉,工作区里的文件留着:
git rm --cached -- .claude/settings.local.json
这一步会在下一次提交里显示成删除。提交之前用 git diff --cached 看一眼,确认走的是索引、不是磁盘。我在临时仓库里跑过:rm --cached 之后磁盘文件还在,git check-ignore 才开始命中。
规则本身,GitGuardian 的建议是把密钥换成变量名,让规则里只留下 $N8N_API_KEY 这种字面量,不要留下值;前缀写窄,不要写成 Bash(curl:*)。官方文档补了一条我这次对得上的:带 && 的复合命令,点「不再询问」时会拆成每条子命令一条规则,最多存 5 条,而不是把整串存成一条。所以文件会比你记得自己点过的次数更碎。
交互点选没跑成,就不把「现在点一次会写出什么样的 JSON」说成我亲眼见过。格式以权限文档为准:永久允许的 Bash 规则进仓库根这个文件,改文件的允许不进。
清单
- 在每个仓库里跑
git ls-files -- '.claude/settings.local.json' '**/.claude/settings.local.json',有输出就是已经跟踪 - 对没跟踪的再跑
git check-ignore -v -- .claude/settings.local.json,没有输出就说明仓库和全局排除都没盖住 - 打开
~/.config/git/ignore(或git config --show-origin --get core.excludesFile指向的文件),确认有**/.claude/settings.local.json - 已跟踪的用
git rm --cached移出索引,再提交;不要只加.gitignore就当完成 -
.gitignore写成.claude/或带**/的文件名,不要以为.claude/*能追溯清理已经跟踪的文件 - 若这个文件进过远端,先轮换里面出现过的凭据,再考虑历史;改忽略规则不会改已经推送的提交和镜像
-
.dockerignore里也列上.claude/,否则COPY . .会把 git 忽略了的文件打进镜像
参考
- GitGuardian,2026-11-09,Claude Code settings.local.json Secrets: 17 months of exposure on GitHub, & Docker Hub
- IT之家,2026-11-10,官方正在修复:Claude Code 被爆把密码写进项目文件,GitHub 上发现 2539 个真实凭据(转 Ars Technica / Apiiro 的口径)
- Ars Technica,2026-11-09,Claude Code is accidentally exposing secrets all over GitHub
- Claude Code 文档,Configure permissions、Settings files and precedence(页面没有单独的发布日期,文中所引句子是 2026-11-11 打开页面时的正文)