点一次「不再询问」,27 个仓库里有 1 个已经跟踪了这个文件

0 阅读12分钟

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.json2 个
已被跟踪,且 git check-ignore 说没忽略1 个,576 字节,16 条 allow
被仓库 .gitignore 盖住1 个,315 字节,4 条 allow,规则是 .claude/
~/.config/git/ignore有这个文件,6 行,没有 settings.local.json

三条可以带走的:

  1. 官方文档写的是:Claude Code 第一次自己写出这个文件时,才会把 **/.claude/settings.local.json 加进全局 git 排除。我这台没加过。文件是你自己建的,或者那次写入没发生,仓库侧就什么都没有。
  2. 已经进了索引的文件,后补 .gitignore 不会让它退出。git check-ignore 对已跟踪文件直接返回「未忽略」,哪怕规则其实写对了。
  3. .claude/* 和 .claude/ 不是一回事,但在「已经跟踪」这件事上结果一样:两条都挡不住索引里的那一份,都要先 git rm --cached。

27 个仓库里这个文件的四种状态

背景

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 忽略了的文件打进镜像

参考