2026 年 9 月底,一位开发者让 Claude Code 重建并清理一个项目。清理出了最坏的结果:AI 沿着 614 个 Windows 目录 junction 一路追删,103 秒删掉了 48,218 个活文件,git 对象库也在爆炸半径里一并损毁——连 git 都救不回来。AI 最后道歉说:“I broke something.”(TechRadar 报道,引自开发者后来删除的 Reddit 帖)
这个案例值得解剖,因为它的失败模式一点都不冷门:这是 NTFS junction、递归删除语义、AI 对”清理”的理解三者的一次普通交互。它也解释了,为什么唯一能在这类事故中稳定幸存的防线,是在 AI 动手之前就拍好的卷级快照。
junction 是什么,程序为什么会在它上面栽跟头
junction(mklink /J)是 NTFS 的重解析点(reparse point),把一个目录路径重定向到同卷的另一处。它在几乎所有目录列表里都长得像目录,但它是个链接,不是拷贝。
关键细节在文件属性里:junction(和符号链接)带着 FILE_ATTRIBUTE_REPARSE_POINT 标志。任何”枚举子项→递归→删除→向上走”的目录树删除逻辑,只要不检查这个属性,就会顺着重解析点钻进目标目录里继续删。Unix 上 rm -r 对符号链接有专门语义(删链接本身、不碰目标);Windows 上自己写的递归没有这个约定——“删掉这个 junction”和”顺着 junction 删下去”之间,只隔着一个属性检查。
清理例程为什么会顺着 junction 删到活数据
一个典型的”清理构建产物”提示,会生成一段对输出目录的递归删除。如果这个目录(或它被遍历经过的地方)里有指向活项目目录的 junction——开发者因为 node_modules 去重、测试夹具、输出重定向、工作区布局等原因,天天在造这种 junction——那么一个不把重解析点当叶子处理的删除例程,删的就是目标文件。
AI 不是恶意的,它只是照着一个看起来很合理的实现去做。junction 让”删这个目录”变成了”把那些文件也删了”。614 个 junction,103 秒,48,218 个文件。
为什么”git 会救你”这次没救成
两个原因,都在这次事故里应验了:
- git 对象库也被删了。 它就在被删的目录树里。版本控制是”被追踪源码”的快照,而它存储的位置就在那棵树里——同一场灾难同时拿走了正本和副本。
- junction 的目标通常在仓库之外。 junction 常被用来把数据重定向出仓库(共享缓存、数据集、用户目录)。这些文件 git 从来就没追踪过。
同样的道理也适用于”git 能兜底”的另两个流行假设:git clean -fd 和 git reset --hard 本身就是 AI 会执行的破坏性命令——Claude Code 的 issue 区里就有一个用这种方式被删掉的、三年的 Unity 项目。
为什么卷级快照能幸存
VSS(卷影副本)快照是卷在某个时间点的视图,实现在存储层。创建快照时,卷上已用块被冻结(写时复制)。之后删文件——哪怕顺着 614 个 junction 删——只影响活文件系统的元数据和块;普通文件操作碰不到快照的存储块,AI 删文件删不到卷影。
恢复就只是一次拷贝:挂载快照,把丢的文件拷回来。unlose 的测试套件里,还原是按字节验证的(SHA256)——只要 AI 动手前有快照,“48,218 个文件”这种场景可以完整找回。
unlose 是做什么的(简短)
unlose 是免费开源(Apache-2.0)的 Windows 服务,在 AI 开始干活时自动拍 VSS 快照:识别 26+ 个 Agent,把快照指令注入它们的记忆文件(Agent 自己也会在危险操作前主动请求快照),再给你一条时间线,按文件或整卷拖回去。它和 DCG 这类命令拦截器是天然互补:拦截器挡它挡得住的,快照接住它漏掉的。
三句话结论
- 审计你所有递归删除的代码路径(包括 AI 替你写的):递归前先查
FILE_ATTRIBUTE_REPARSE_POINT。 - junction + AI Agent,值得像四十年前 Unix 时代对待符号链接那样被严肃对待——语义鸿沟是真实的,而且已经吃掉一个生产项目。
- 把最后一道防线放在爆炸半径之外:git 在树里,云同步会同步删除。AI 动手之前拍好的卷级快照,是灾难唯一碰不到的副本。