一个 Git 配置键在七个编程 Agent 中执行了代码。2022 年的修复方案拦不住它

同一段 payload,三种配置。被广泛引用的 2022 年缓解措施,并没有覆盖通往 AI 编程 Agent 的那条投递路径。
2026 年 9 月 1 日,Manifold Security 的 Francisco Rosales 公布了 GitSpawn:在七个 AI 编程 Agent 上发现的八条代码执行路径,全部基于同一个原语。¹ 受影响的产品包括 Claude Code、Codex、Qwen Code、Goose、Grok Build 和 Hermes。研究发布时,八条路径中仍有四条尚未修复。
这个原语是一个叫 core.fsmonitor 的 Git 配置键。它的值是一条 shell 命令。Git 在常规的索引刷新过程中会执行它,也就是在 git status、git diff,或者 Agent 为了弄清楚仓库当前状态而执行的十几种操作之中。这条命令以开发者的身份运行,父进程是 git,而不是 Agent。
往下看之前,先说明一个决定这事跟你有没有关系的限制条件。光克隆一个恶意仓库是不够的。 Rosales 说得很明确:「克隆一个恶意 URL 不会有任何作用,fetch 和 pull 也一样。仓库必须以文件的形式到来,而且里面已经带着 .git 目录。所以攻击面是所有搬运目录而不是克隆目录的手段:共享的 .zip 压缩包、共享网盘、同步文件夹、U 盘。」¹
这是个实打实的限制,它把攻击范围缩小了不少。但它并没有消除风险。开发者无时无刻不在收到别人发来的目录,而且越来越多的人把这些目录直接交给 Agent,然后就走开了。
不新鲜的那部分
GitSpawn 有意思的地方,不是这个原语能用。而是这个原语能用这件事,我们已经知道四年了。
Justin Steven 在 2022 年发布过一份安全公告,公开了 core.fsmonitor 的滥用手法,针对的是“藏起来的裸仓库”。他讲清了攻击原理,演示了任意代码执行,并点明了与 Rosales 在 2026 年重申的相同交付限制:“这类问题无法通过 git clone 克隆的仓库来利用,因为克隆仓库时,远程端无法充分控制客户端 .git/ 目录里的文件。”
他也记下了 git 官方的立场,这一立场至今未变:这些是特性,不是漏洞,“git 目前没有计划改变针对仓库 .git/config 中潜在危险配置指令的现有行为。”
随后,2022 年 10 月 3 日,git 2.38.0 发布了一项缓解措施。把 safe.bareRepository 设为 explicit,git 就会拒绝操作它在工作树内发现的裸仓库。Steven 的公告用粗体告诉读者,强烈建议考虑设置它——这也是大多数评论文章会提到的配置项。
它需要用户主动开启,四年后的今天依然如此。而且,对于影响 AI 编码代理的攻击路径来说,它是错误的修复方案。这一点值得演示,而不是空口断言。
三种配置,同一个载荷
我搭建了尽可能最小、也最贴近真实的复现环境:一个自带 .git 目录的文件夹,正是 Rosales 描述的那种交付方式,里面携带了 core.fsmonitor = touch /tmp/fsmonitor-poc-fired; echo ""。然后在 git 2.43.0 上以三种方式运行 git status,检查标记文件是否被触发。
$ git status --porcelain # stock git
PAYLOAD FIRED
$ git -c safe.bareRepository=explicit status --porcelain
PAYLOAD FIRED # the 2022 mitigation
$ git -c core.fsmonitor= status --porcelain
BLOCKED
2022 年的缓解措施没能拦住它。这不是缓解措施本身的缺陷。safe.bareRepository 防的是埋在工作树里的裸仓库,那只是这类攻击的一种交付路径。对于一个完全普通的仓库、只是自带了恶意配置的情况,它无能为力——而当打开目录的是一个代理时,这才是真正关键的路径。
为求严谨,我确认了这项缓解措施在应当生效的地方确实有效。把 payload 放进一个深藏的裸仓库后,git 会直接拒绝:
fatal: cannot use bare repository '/tmp/.../buried.git'
(safe.bareRepository is 'explicit')
两个变种、两道防御,而那个被广泛引用的设置,覆盖的恰好不是这里的问题。
一台工作机上实际配置了什么
2026 年 9 月 9 日,我检查了自己的开发机。系统是 Ubuntu,git 2.43.0,每天在用 Claude Code 和 Codex。
$ git --version
git version 2.43.0
$ git config --system --get safe.bareRepository; \
git config --global --get safe.bareRepository; \
git config --local --get safe.bareRepository
(nothing)
三个作用域全都没有设置。这是默认状态,不是配置错误。Debian 和 Ubuntu 发布的 git 都不带这个设置,正常安装流程也不会把它打开。
于是问题变成:既然开发者这边的 git 显然不会防御,那 agent 会不会自己防御?每个工具大约三十秒就能验证,而上面测试输出的第三行才是关键——在调用时把 core.fsmonitor 清空,才是真正管用的做法。
| 工具 | 版本 | safe.bareRepository |
core.fsmonitor |
|---|---|---|---|
| Claude Code | 2.1.263, 2.1.265, 2.1.266 | 存在,设为 explicit |
存在,已清空 |
| Codex CLI | 0.153.4 | 不存在 | 不存在 |
| git 默认 | 2.43.0 | 未设置 | 从仓库配置中读取 |
任何人都能复现这个结果。用 strings 扫一遍 agent 的二进制文件,grep 这两个键,再和 git config --get 的输出对比。Claude Code 里带着字面字符串 safe.bareRepository=explicit 和 core.fsmonitor=,还有 -c core.hooksPath=/dev/null。这是一个很明显的模式:它用 -c 覆盖参数加固自己发起的 git 调用,而不是信任环境里的配置。
Codex 里这两个键一个都没出现——但要是因此断定 Codex 没有防护,那就错了,而且错得很容易。Codex CLI 确实受影响,OpenAI 已经修好。CVE-2026-19592 覆盖 0.102.0 到 0.130.0,在 0.131.0 中修复,macOS 和 Windows 上的 Codex Desktop 也有配套公告。OpenAI 自己的说明把这类问题为什么麻烦讲得很清楚:“helper 在 Codex 的命令沙箱之外运行,也不弹用户审批提示,攻击者控制的代码因此能以用户权限执行。”
所以两家厂商在严重性上并无分歧,分歧在于修复思路的层面。OpenAI 是直接堵死这条路径。Anthropic 按我能测到的版本,是在每次调用 git 时把那个危险的键置空——我的第三项测试表明,真正挡住这个攻击向量的正是这一步。它同时还带着 safe.bareRepository=explicit,把“埋藏”变体也一并覆盖了。
这才是正确的组合,而据我所知,没人公开这么说过。公告里写的是版本号,评论里说的是 2022 年那个设置,两边都没提到那行只需一个开关的调用参数——开发者,或者任何会调用 git 的其他工具,今天下午就能用上。
这张表有一个必须说明的局限。我只能检查自己磁盘上存在的版本,最早的是 2.1.263。Rosales 报告第二条 Claude Code 攻击路径在 2.1.252 上可用,所以缓解措施应该是在中间这十一个版本里的某个版本落地的。我没法确定具体是哪一个,也不打算声称 Anthropic 是针对这份报告才发布的。
披露记录
厂商的回应,才是这件事不再只是一个 git 故事的地方。
| 智能体 | 报告时间 | 厂商回应 | 修复状态 | 被 grith 拦截 |
|---|---|---|---|---|
Claude Code(core.fsmonitor) |
2026 年 6 月 26 日 | 与同日一份报告重复 | 已在 2.1.196 中修复 | 是 |
| Codex CLI | 2026 年 9 月 1 日披露 | OpenAI 发布了三个 CVE | 已在 0.131.0 中修复,CVE-2026-19592 | 是 |
| Qwen Code | 2026 年 7 月 7 日 | 阿里 SRC 已受理 | 0.22.3 版本仍未修复 | 是 |
| Goose | 2026 年 7 月 13 日 | 已确认 | 已在 1.44.0 中修复,CVE-2026-72718 | 是 |
| Grok Build | 2026 年 7 月 14 日 | 以与早前报告重复为由关闭 | 1.0.13 版本仍未修复 | 是 |
Claude Code(ultrareview) |
2026 年 7 月 15 日 | 与内部工单重复 | 2.1.252 版本仍未修复 | 是 |
| Hermes | 2026 年 7 月 20 日 | 未做分类处理 | 0.21.0 版本仍未修复,CVE-2026-71963 | 是 |
来源:Manifold 公布的表格,以及 The Hacker News 转述的 OpenAI 安全公告。最后一列每一行都一样,这是有意的。它并不代表厂商的什么属性,背后的复现过程放在本文末尾。
表里三行写着「重复」。这个词承担了太多含义。一份被以「重复」为由关闭的报告,意味着它被读过了、和某个已知问题对上了号,然后就没有下文了。Grok Build 的那份被以「与早前报告重复」关闭,之后一直没修。Claude Code 的第二条路径在 7 月被以「与内部工单重复」关闭,而 Rosales 在 9 月仍然能利用它。
Hermes 根本没走分类流程,它的 CVE 是在没有厂商参与的情况下分配的。
Anthropic 和 OpenAI 在这件事上都表现得不错,这一点值得直说。Anthropic 从收到报告到修好第一条路径只用了三天,而按我能测到的版本,它现在每次调用 git 都会把这个危险配置键清空——我的第三项测试表明,这才是真正堵住这个攻击途径的做法——此外还设置了 safe.bareRepository=explicit,把“埋雷”那种变体也一并覆盖了。OpenAI 堵住了这条路径,在三个产品上都推送了修复并附带了 CVE 编号,还坦率说明了自家的沙箱为什么没起作用。所以两家厂商在严重性判断上并没有分歧,分歧在于修复要修到哪一层。这才是正面案例该有的样子,也让上面那三行“重复”记录更难被原谅,而不是更好被原谅。
这才是正确的组合,而据我所知,还没有人公开这样说过。安全公告写的是版本号,评论文章写的是 2022 年那个配置,两边都没提到那个只需一行就能加上、开发者或其他任何调用 git 的工具今天下午就能采用的启动参数。
关于那张表,有一个需要如实说明的局限:我只能检查自己磁盘上存在的版本,最老的是 2.1.263。Rosales 报告第二条 Claude Code 路径在 2.1.252 上还能复现,所以修复应该落在中间这十一个版本里的某处。我无法确定具体是哪一个,也不打算声称 Anthropic 是针对这份报告才推送的。
为什么现有防护措施拦不住
审批提示。 智能体会在识别到风险命令时先询问用户。它执行的是 git status,这本身毫无风险,而恶意载荷并不在命令里。
沙箱和允许列表。 载荷并不是以智能体身份执行的,而是作为 git 的子进程执行,而 git 是智能体正常调用的。针对智能体自身进程的防护看不到它;而把 git 放进允许列表的防护,恰好把投递工具也放进了白名单。
仓库信任提示。 VS Code 在 2021 年就吃过这个亏,现在运行 git 前要求工作区已被信任。2 终端里的智能体没有对应机制,从头到尾就没有任何时刻问过用户:这个目录是否可信。
扫描器。 这里没有恶意软件。只有一个配置文件,里面有一个合法的键和一个字符串值——配置文件本来就该长这样。
谁能看见它
上面所有控制手段都位于代理之上,或与代理同级。下面这个则位于代理之下。
grith 在操作系统层面监管代理。它在代理的整个进程树上安装 seccomp-BPF 预过滤器和 ptrace 监督器,因此每个后代进程都会继承这份监管:代理本身、它启动的 shell、shell 运行的 git,以及 git 根据 core.fsmonitor 执行的那条命令。这个载荷之所以对代理不可见——因为它是以孙进程而非代理的身份执行——恰恰也是它无法逃过 grith 的原因。没有任何东西能绕过系统调用。
我在一个运行中的 grith 会话里复现了这次攻击,并读取了审计日志。6 被投放的仓库带有 core.fsmonitor = cat ~/.ssh/<key> > exfil,这是对真实载荷窃取凭据行为的替代品,指向一个诱饵密钥。仅仅运行一次 git status 就触发了它。grith 完全独立地给载荷发起的四次系统调用打了分,触发 git status 的代理对此毫不知情:
| 载荷尝试的操作 | grith 的判定 |
|---|---|
在 ~/.ssh 下创建文件 |
排队——冻结等待审查 |
对该文件执行 chmod |
拒绝——返回 EPERM |
| 读取私钥 | 排队——冻结等待审查 |
| 事后删除证据 | 拒绝——返回 EPERM |
两次修改操作被直接拒绝,系统调用返回值中注入了 EPERM,没有任何提示。对 ~/.ssh 的读取和写入则被排队,在 grith 中这意味着进程会冻结,在系统调用继续之前会询问人工,并明确说明操作内容:一个 git status 想要读取你的 SSH 私钥。在这次运行中,我故意批准了读取,以确认字节确实会流出,且判定是真实生效而非做样子。这次测试的重点是判定结果,而不是拦截。在非交互式或 CI 会话中,排队操作会默认拒绝,而不是等待一个永远不会到来的回答。
这些结果和是哪个 Agent 调用的 git 无关。grith 位于进程树之下,而不是藏在某个工具内部;所以无论 git 调用来自 Claude Code、Codex,还是上表里的任何一行,结论都一致。这也解释了披露记录里为什么最后一列始终不变,而厂商那一列能从「三天修复」一路排到「从未处理」。
这道缝隙落在哪里
从 GitSpawn 里真正值得记下的,不是七家厂商发了同一个 bug,而是一个四年前的老问题,被一个并不掌握四年前上下文的行业重新发现,然后拿四年前的缓解措施做了部分回应——而这个措施覆盖不到新的投递路径。
Rosales 没有引用 Steven 的公告,也完全没提 safe.bareRepository。我读过的报道里同样没有。这不是在批评这项研究——它做得仔细、文档齐全、披露也合规。只是说明一个事实:安全知识和工具是在两个不同的房间里长大的,而这类问题总是出在两者的接缝处。
底下有个更结构性的问题,和 MCP 工具投毒、Agent 技能注册表里出现的是同一个:不可信输入到达某个工具,工具照着它行动,而最终真正执行的东西,离任何人以为需要评估的位置还差着好几个进程。一个会调用外部命令的 Agent,会继承它调用的每个东西的全部风险;而它自己的权限模型只到自己进程边界为止。
这就引出一个让人不太舒服的问法:如果 Agent 的工作本身就是运行别的程序,那「给 Agent 做沙箱」到底指什么?不管答案是什么,它都不可能是围着 Agent 砌一堵墙——因为 Agent 每次调用外部命令,都把自己的权限直接穿过这堵墙带出去。沙箱必须位于 Agent 启动的一切东西之下,卡在载荷无法再往下钻的那条边界上。这正是 grith 在系统调用层拦截、而不在工具内部拦截的全部原因。
脚注
-
Francisco Rosales,Manifold Security,《GitSpawn:单个漏洞即可让不可信仓库在 Claude Code、Codex、Cursor 和 Grok 中执行代码》,2026 年 9 月 1 日。https://www.manifold.security/blog/ai-coding-agents-git-hijack
-
Justin Steven,《Git 隐藏裸仓库与 fsmonitor:各种滥用方式》,2022 年。https://github.com/justinsteven/advisories/blob/main/2022_git_buried_bare_repos_and_fsmonitor_various_abuses.md
-
Git 2.38.0 版本发布,2022 年 10 月 3 日,引入
safe.bareRepository。https://git-scm.com/docs/git-config#Documentation/git-config.txt-safebareRepository -
复现运行于 git 2.43.0、Ubuntu,2026 年 9 月 9 日。载荷为一个无害的
touch标记,置于临时目录中,无网络访问。 -
The Hacker News,《恶意 Git 配置可让 Claude 及其他 AI 编程代理运行攻击者代码》,2026 年 9 月 2 日。https://thehackernews.com/2026/09/malicious-git-configs-can-make-claude.html
-
grith 复现,2026 年 9 月 9 日,在一台由 grith 监管运行 Claude Code 的机器上。
core.fsmonitor载荷读取的是为测试放置的诱饵私钥,而非真实凭据,并写入本地标记,无网络外连。判定结果取自 grith 自身的审计日志。该防护取决于 grith 在何处实施,而非由哪个代理调用 git,因此尽管只在一个代理下复现,仍被视为对披露记录中每一行均成立。