Windows Junction 让 AI 代理越界删除 48218 个文件

Claude Code 代理误用 os.walk,穿过 Windows junction 删除 48,218 个活跃文件并清空 git 对象库。
文章基于单一信源的事故报告,复盘一个 Claude Code 子代理在 103 秒内删除约 48,218 个活跃项目文件、清空 .git/objects、refs 与 logs 的过程。事故机制有据可查:删除器用 `os.walk(followlinks=False)` 清理镜像副本,但 `os.path.islink()` 对 Windows junction 返回 False,遍历因此进入 614 个 junction 指向的活跃目录树。文章解释 NTFS junction 与符号链接的区别、Python 3.12 才新增 `isjunction()`、`os.walk` 至今无 junction 检查,并对比 shutil.rmtree、PowerShell、git clean 等工具的行为差异。此外,worktree 不隔离磁盘、共享同一对象库,原生 Windows 上 Claude Code 又不经沙箱运行,共同放大了损失。防护措施包括删除前 realpath 加 inside() 检查、频繁推送、WSL2/容器隔离、审批提示与异地备份。
Windows Junction 让 AI 代理越界删除 48218 个文件
一份事故报告描述了一个令人不安的场景:一个 Claude Code 代理在 103 秒内删除了约 48,218 个活跃项目文件,并清空了 git 对象库。根据 nope.net 的事故记录和 Cyber Security News 的报道,事故发生在 10:10:31 至 10:12:14(美国东部时间)之间。用户声称已明确授权代理在隔离副本上进行构建、修正和测试,但副本实际上并不隔离。

需要提前说明一个关键前提:所有报道都源自单一信源——用户的 Reddit 帖子、代理自己的报告以及附带的验证报告。AI 事故数据库将其记录为第 1719 号事件,日期标注为 2026 年 9 月 19 日,并注明该记录尚未经过独立验证。无论那台机器上究竟发生了什么,报告描述的机制是有据可查的真实行为:一个古老的 Windows 文件系统特性和一个 Python 函数,后者严格按照其文档所述执行操作。你可以在临时文件夹里两分钟内复现危险的那一半。
事故链条:代理是如何越界的
根据 Cyber Security News、nope.net 和 AIID 条目的交叉印证,事件链条如下:
代理启动子代理来重建项目镜像。现有构建脚本无法原地刷新镜像,子代理于是编写了一个 Python 删除器来清理旧副本。旧镜像包含 7,332 个真实文件和 614 个指向活跃目录树的 Windows 目录联接(junction)。
删除器使用了 os.walk(..., followlinks=False),显然以为该标志会阻止它进入链接文件夹。验证者发现 os.path.islink() 对 junction 返回 False。一个保护逻辑只覆盖了每个 junction 根目录下的直接文件,所有嵌套更深的内容都被遍历并删除。
删除日志记录了 55,550 个文件。减去 7,332 个预期删除的镜像文件,验证者计算出有 48,218 个活跃文件被误删。.git/objects、refs 和 logs 目录被清空,git log 找不到任何提交。索引虽然幸存并仍列出 7,221 条路径,但背后的 blob 已全部消失。据 nope.net 报道,用户后来在评论中表示希望 Windows 卷影副本或异地备份能恢复数据。目前尚未有结果报告。所有报道均未提及 Claude Code 版本、模型和权限模式,Anthropic 也未对此作出回应。
Junction 是一扇门,遍历工具未必看得见
NTFS 支持三种链接:硬链接、junction 和符号链接。微软文档将 junction 定义为目标为目录的链接,可以跨同一计算机上的不同本地卷,通过重解析点实现。Junction 不能指向单个文件或映射的网络驱动器。
创建 junction 使用 mklink /J,目录符号链接则是 mklink /D。打开任何一个,看到的都是目标文件。但对程序而言,“这是链接吗”的答案取决于哪个函数在问。在 Python 中,os.path.islink() 回答的是“这是符号链接吗”——junction 不是。Python 3.12 新增了 os.path.isjunction()。而 os.walk 在其当前源代码中,用 islink() 或 DirEntry.is_symlink() 决定是否进入子目录,完全没有 junction 检查。
所以 followlinks=False 能让 os.walk 绕开符号链接,却会径直穿过 junction。这在 Windows 上并非新闻:2014 年 UrBackup 的一篇博文就描述过 os.walk 在 junction 中循环、islink 对它们返回 False 的情况。Junction 还会出现在你意想不到的地方——pnpm 的 FAQ 说明,在 Windows 上如果开发者模式关闭,node_modules 内部会用 junction 代替符号链接。
**重解析点(reparse point)**是 NTFS 的一项底层机制:文件或目录可以携带一段由特定文件系统过滤器驱动解释的元数据,操作系统在解析路径时会调用该驱动改变最终落点。junction、符号链接、OneDrive 的按需文件、部分去重与备份产品都建立在重解析点之上。因为路径解析发生在文件系统驱动层,许多用户态 API 看到的仍是“真实目录”,只有当调用方显式查询 FILE_ATTRIBUTE_REPARSE_POINT 属性并读取重解析标签时,才能发现这其实是一个链接。这正是 os.path.islink() 与 junction 错位的根源:该函数历史上依据 POSIX 符号链接语义实现,而 Windows 的 junction 属于另一类重解析标签,因而被判定为普通目录。
os.walk 的 followlinks 参数名字容易造成误解。“links”在这里特指符号链接,函数内部通过 islink() 或 DirEntry.is_symlink() 判断是否递归,参数为 False 时只是不去跟随符号链接,并不构成一道通用的“不跨越任何链接”的防线。要真正拦截 junction,需要额外用 os.path.isjunction()(Python 3.12+)、读取 st_reparse_tag,或在删除前统一用 os.path.realpath() 解析并做包含性检查。
哪些删除工具会穿过这扇门
不同工具的行为差异很大:
| 工具 | 遇到 junction 时的行为 |
|---|---|
Python os.walk(followlinks=False) | 会进入。使用 islink(),对 junction 返回 False |
Python shutil.rmtree | 自 3.8 起,在 Windows 上删除 junction 本身而不删除其内容 |
PowerShell Remove-Item -Recurse | 相关 bug 在 PowerShell 6 中标记为已修复,但有报告称 Windows PowerShell 5.1 仍会跟随 junction |
Git for Windows git clean | 曾会穿 junction 删除,2019 年的修复阻止了它遍历挂载点 |
cmd /c rmdir /S /Q | 被建议作为不跟随 junction 的删除方式 |
同一机制在 2026 年 2 月已出现过一次。有用户要求 Claude Code 从 pnpm monorepo 中移除两个 git worktree,递归删除(Remove-Item -Recurse -Force,也可能是 Git Bash 的 rm -rf)跟随了 node_modules 中的 junction,永久删除了用户配置文件和源代码中的文件夹。该问题被标记为 data-loss 和 has repro,机器人以“不计划修复”为由关闭。同一种机制,七个月前,触发场景是清理 worktree。
递归删除工具的行为差异,本质上取决于它们如何判断“是否跨过链接边界”。 POSIX 语义下,符号链接与目录是两种明确区分的对象,rm -rf 删除链接本身而不进入目标,因此绝大多数 Unix 工具默认安全。Windows 的兼容层把 junction 呈现为普通目录,只有专门识别重解析点的实现才能正确规避。Python 的 shutil.rmtree 自 3.8 起在 Windows 上加入了针对 junction 的特判,删除链接本身而保留目标内容,这是社区反馈推动的结果;git clean 在 2019 年的修复同样是为避免遍历挂载点。
反过来说,任何自行拼接遍历逻辑的代码——包括 AI 代理现场生成的删除脚本——都不会自动继承这些修复。跨平台工具在 Windows 上尤其容易踩坑,因为它们通常按 POSIX 假设编写,测试环境又多为 Linux/macOS。判断一个删除路径是否安全,可靠做法不是依赖工具名或参数,而是先解析(os.path.realpath)、再确认解析结果仍位于允许清理的根目录之内,最后才执行删除。
为什么“隔离”的 worktree 并不隔离磁盘
许多人推荐用 worktree 让并行代理互不干扰文件。但 worktree 并不隔离磁盘。
git worktree 文档说明,新 worktree“共享除每工作树文件(如 HEAD、index 等)之外的所有内容”。在链接的 worktree 内部,$GIT_COMMON_DIR 指向主 worktree 的 $GIT_DIR。此次事故丢失的 objects、refs 和 logs 正是该仓库所有 worktree 共用的。给代理一个独立 worktree,它获得独立分支和磁盘文件,但仍然持有指向共享对象库的路径,git 本身不会阻止进程跟随它。
Claude Code 自己的沙箱文档也承认这一点:当工作目录是链接 worktree 时,Bash 沙箱“还允许写入主仓库的共享 .git 目录”。而且该沙箱仅在 macOS、Linux 和 WSL2 上运行——“在原生 Windows 上,Claude Code 不通过沙箱运行命令”。其检查点功能不追踪 Bash 命令更改的文件,通常也不恢复子代理的编辑。
链接 worktree 的目录布局值得展开。执行 git worktree add 时,新目录下只有一个 .git 文件,内容形如 gitdir: /path/to/main/.git/worktrees/<name>。真正的元数据位于主仓库的 .git/worktrees/ 下,而对象库 .git/objects、引用 .git/refs、日志 .git/logs 等共享部分由 $GIT_COMMON_DIR 指向。$GIT_DIR 则指向每工作树私有的目录,存放 HEAD、index 等。这意味着 worktree 提供的是分支与工作区文件的隔离,而非存储层的隔离:任何一个进程只要拿到某个 worktree 的路径并沿着 .git 走,就能抵达整个仓库共用的对象库。
这也解释了为什么事故中的损失是全局性的。索引文件作为每工作树私有数据幸存下来,仍列出 7,221 条路径,但索引引用的 blob 位于共享对象库中,而对象库已被清空,于是 git log 找不到任何提交、工作区无法重建。换句话说,worktree 让你在逻辑上分叉,却在物理上共用同一份历史。
真正的限制措施
按优先级排序,以下措施能限制爆炸半径:
频繁推送代理的分支。 远程仓库上的提交能挺过本地 .git 被清空。事故中的索引幸存了,但毫无帮助。
在 Windows 上,将代理运行在 WSL2 内,那里有 Claude Code 的 Bash 沙箱(默认关闭,用 /sandbox 开启),或者对无人值守任务使用容器或虚拟机。
将 shell 删除置于审批提示之后,并检查你的权限模式会自动批准什么。2 月的事故中,Accept Edits 是开启的。
教会你自己的脚本执行 inside() 检查。 代理会从你的仓库复制模式。安全做法是:删除路径前先解析它,确认它仍在你想清理的文件夹内。os.path.realpath 自 Python 3.8 起在 Windows 上能解析 junction。对所有即将删除的文件运行 inside() 检查,返回 False 时拒绝删除。指向另一驱动器的 junction 会让 commonpath 抛出异常,因此该情况也应视为外部。
保留异地备份。 这是事故中用户最后的希望。
值得留意的是,我们所知的这起事故几乎全部来自代理自己的报告和删除日志。如果你希望拥有不依赖代理自述的、关于代理行为的记录,就需要专门的追踪与评估工具来提供独立视角。
inside() 检查的实现思路可以具体化为:先用 os.path.realpath() 把待删路径解析为最终真实路径,同时把清理根目录也解析一遍;然后用 os.path.commonpath() 求两者的公共前缀,判断它是否等于解析后的根目录。相等即在内部,可以删除;否则拒绝并记录告警。需要注意几个边界情况:当 junction 指向另一个驱动器时,commonpath 会因盘符不同抛出 ValueError,这本身就应视为“在外部”;大小写与尾部分隔符差异需要归一化处理;对不存在的路径要先做存在性判断,避免异常中断整个清理流程。
这套检查的价值在于它不依赖特定 API 对链接类型的认知,而是基于路径解析后的实际落点做判断,因此对 junction、符号链接、硬链接目录入口都成立。把它固化成一个共享的删除辅助函数,并要求代理在写清理脚本时复用仓库中已有的模式,比事后审查每一次删除更可持续。
相关推荐

前端转AI Agent开发:一线架构师解析职业路径与红利
一线AI架构师解析前端转AI Agent开发的行业变化与落地路径:纯前端岗位为何消失、Agent+全栈为何成为招聘主流,以及Agent开发工程师、FDE、远程接单三条转型路线和对应学习策略。

AI Agent开发课程怎么选?五条避坑指南
机械转行开发者踩坑两年总结的AI Agent开发课程筛选清单:完整交付链路、可写进简历的项目、真实开发视角、更新频率、陪跑反馈五个维度,附具体判断方法和避坑要点。

扣子(Coze)智能体搭建入门:零代码开发AI Agent实战指南
扣子(Coze)是字节跳动推出的一站式AI应用开发平台,支持零代码拖拽搭建AI智能体。本文基于B站教程梳理扣子的核心概念、三大优势、国内版与国际版差异,以及智能体与应用的区别,帮助初学者快速入门AI Agent开发。