
1. 103秒删掉4.8万个文件这事到底怎么发生的先把这件事的核心事实摆出来一个基于 Claude Code 的 Agent 在 Windows 环境下执行任务时用 103 秒删除了 4.8 万个文件而且连.git目录都没能幸免。这不是段子是真实发生的工程事故。我第一时间看到这个消息的时候第一反应不是AI 太危险了而是这个目录结构一定有问题。为什么这么说因为在 Windows 上一个正常的项目目录哪怕你装了node_modules文件数量撑死也就几万个但要在 103 秒内被批量删除说明删除操作走的是一条极短路径——它没有逐层遍历而是直接命中了一个看起来像普通文件夹、实际上是整个盘符入口的东西。这个东西就是Directory Junction目录联接。Directory Junction 是 NTFS 文件系统的一个特性你可以把它理解成 Windows 版的软链接但它比符号链接更底层、更透明。对绝大多数程序来说Junction 和真实目录没有任何区别——你cd进去、你dir列文件、你Remove-Item -Recurse递归删除操作系统都不会给你任何警告。问题就出在这个透明上。我举个具体的场景你就明白了。假设你的项目放在D:\projects\myapp但你的依赖缓存、构建产物、甚至整个用户目录被 Junction 到了C:\Users\xxx。当 Agent 拿到一个清理临时文件的任务它扫描到D:\projects\myapp\temp发现这是个目录于是执行递归删除。如果temp恰好是一个指向C:\Users\xxx的 Junction那么 Agent 以为自己在删项目里的临时文件实际上它在删你整个用户目录下的所有东西——包括.git、包括配置、包括其他项目。103 秒删 4.8 万个文件平均每秒 466 个文件。这个速度说明删除操作几乎没有遇到任何阻碍没有权限拦截、没有文件占用、没有回收站确认。这三点在 Windows 上同时成立通常意味着操作是以高权限运行的而且用的是底层删除 API比如DeleteFile直接调用而不是走资源管理器。这里有个很多人忽略的细节.git目录为什么也会被删因为.git在文件系统层面就是一个普通目录它没有任何保护标记。Agent 不会因为这是版本控制目录就手下留情——它根本不知道.git意味着什么。这就是当前 Agent 架构的一个根本性缺陷它理解语义但不理解文件系统的物理边界。2. Agent 的理解力和文件系统的物理边界之间的鸿沟2.1 Agent 眼里的目录树和操作系统眼里的目录树不是一回事这是整件事最核心的技术点也是我觉得最值得所有做 Agent 开发的人反复咀嚼的地方。当 Claude Code 这类 Agent 拿到一个任务比如帮我清理项目里的临时文件它的工作流程大致是这样的先通过工具调用通常是 shell 命令或文件系统 API列出目录结构然后基于语言模型的理解判断哪些是临时文件最后执行删除。问题在于它看到的目录树是逻辑视图不是物理视图。逻辑视图是什么就是ls或者dir命令输出的那个树状结构。在这个视图里Junction 就是一个普通文件夹符号链接也是一个普通文件夹。Agent 看到temp/下面有一堆文件它不会知道这些文件实际存储在另一个物理位置。物理视图是什么是 NTFS 的 MFT主文件表里记录的真实扇区分布。在物理视图里Junction 是一个重解析点Reparse Point它有一个特殊的标记位告诉操作系统这个目录的内容不在本地请跳转到另一个路径。绝大多数文件操作 API 默认工作在逻辑视图下。Remove-Item -Recurse、rm -rf、shutil.rmtree这些命令在遇到 Junction 或符号链接时行为是不一样的工具/命令遇到 Junction 的默认行为风险等级rm -rf(Git Bash)跟随链接删除目标内容极高Remove-Item -Recurse(PowerShell)跟随链接删除目标内容极高shutil.rmtree(Python)跟随链接删除目标内容极高rd /s /q(CMD)跟随链接删除目标内容极高robocopy /MIR默认不跟随需/SL参数低fsutil reparsepoint只操作重解析点本身安全你看几乎所有顺手的删除命令默认都是跟随链接的。这就是为什么 103 秒能删掉 4.8 万个文件——Agent 大概率用了一条rm -rf或者Remove-Item -Recurse而这条命令恰好命中了一个 Junction。2.2 为什么 Agent 特别容易踩这个坑人类工程师也会踩 Junction 的坑但人类有几个天然的保护机制第一人类在删除前会犹豫。看到temp目录人类会想这个 temp 是什么时候建的里面有什么这种犹豫虽然低效但救命。第二人类对路径长度有直觉。如果D:\projects\myapp\temp这个路径下的文件数量异常多人类会警觉。但 Agent 不会它只会觉得任务要求清理我清理就是了。第三人类有上下文记忆。你可能记得三天前自己手动建过一个 Junction 来做测试。但 Agent 的上下文窗口里没有这个信息除非你明确告诉它。第四也是最关键的Agent 被设计成高效执行。它的目标函数里完成任务的权重远高于谨慎操作。当你给它一个清理任务它会用最直接的方式达成目标而不是用最安全的方式。我实测过几个主流的 Agent 框架在 Windows 环境下处理文件删除任务时默认行为都偏向激进。有的框架甚至会在 prompt 里写尽量用一条命令完成操作这直接鼓励了 Agent 使用rm -rf这种大杀器。2.3 Directory Junction 在 Windows 上的特殊地位为什么这个问题在 Windows 上特别突出因为 Windows 的 Junction 使用场景比 Linux 的符号链接更隐蔽。在 Linux 上符号链接通常有明显的视觉标识——ls -l会显示-颜色也不一样。而且 Linux 社区对符号链接的风险有广泛认知很多工具默认不跟随链接。但在 Windows 上Junction 的创建非常简单mklink /J一条命令就搞定而且创建出来的东西在资源管理器里看起来和普通文件夹完全一样。没有箭头标识没有特殊图标双击进去和普通目录无异。更麻烦的是Windows 上有大量系统级和工具级的 JunctionC:\Users\All Users是指向C:\ProgramData的 JunctionC:\Documents and Settings是指向C:\Users的 Junction很多开发工具如 Docker Desktop、WSL会在用户目录下创建 Junction一些包管理器如 Chocolatey、Scoop也会用 Junction 来做版本切换这意味着即使你的项目目录本身很干净只要它下面有任何一层是 Junction删除操作就可能越界。3. 从这次事故里能提炼出的具体防护措施3.1 删除前的物理边界检查如果你在做 Agent 开发或者你在用 Agent 做自动化任务第一件要做的事是在删除任何目录之前检查它是不是重解析点。在 Windows 上可以用 PowerShell 这样检查function Test-ReparsePoint { param([string]$Path) $item Get-Item -LiteralPath $Path -Force return ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 }这个函数的核心是读取文件的Attributes属性然后和ReparsePoint标志位做按位与运算。如果结果非零说明这个目录是一个重解析点Junction 或符号链接删除时必须特别小心。更安全的做法是在 Agent 的工具层直接禁用跟随链接删除。比如你自己封装一个删除函数import os import stat def safe_rmtree(path): if os.path.islink(path): os.unlink(path) return for root, dirs, files in os.walk(path, topdownFalse): for name in files: fp os.path.join(root, name) if not os.path.islink(fp): os.remove(fp) for name in dirs: dp os.path.join(root, name) if os.path.islink(dp): os.unlink(dp) else: os.rmdir(dp)这段代码的关键在于遇到链接时只删除链接本身不递归进去。os.path.islink在 Windows 上对 Junction 的检测可能不完美更可靠的方式是用os.lstat检查st_reparse_tag或者直接调用 Windows API。3.2 给 Agent 加一道删除确认闸门我在自己的 Agent 项目里加了一个硬性规则任何删除操作如果目标路径下的文件数量超过 100 个必须先输出预览等待人工确认。这个规则的实现很简单在工具调用层加一个拦截器def delete_with_guard(path, max_files100): file_count sum(len(files) for _, _, files in os.walk(path)) if file_count max_files: preview generate_preview(path, limit20) raise NeedsConfirmation( f即将删除 {file_count} 个文件预览如下\n{preview} ) return do_delete(path)这个闸门救过我至少两次。一次是 Agent 想删一个node_modules另一次是它想清理一个被 Junction 指向的缓存目录。两次都是因为文件数量异常触发了确认我才发现路径不对。3.3 用白名单而不是黑名单来限定操作范围很多 Agent 框架用的是黑名单策略禁止删除C:\Windows、禁止删除C:\Program Files等等。但黑名单永远列不全而且 Junction 可以让任何路径变成敏感路径。更可靠的是白名单只允许 Agent 在指定的工作目录内操作。具体做法是在每次文件操作前把目标路径解析成绝对路径然后检查它是否以工作目录为前缀def is_within_workspace(target, workspace): target os.path.realpath(target) workspace os.path.realpath(workspace) return os.path.commonpath([target, workspace]) workspace注意这里用了os.path.realpath它会解析掉所有的符号链接和 Junction得到真实的物理路径。这一步至关重要——如果不用realpath一个指向外部的 Junction 就能轻松绕过白名单检查。3.4 Git 仓库的额外保护.git目录被删是这次事故里最让人心疼的部分因为代码本身可能还有备份但 Git 历史一旦丢失就很难恢复。我的做法是给.git目录加一层额外的保护在 Agent 的工具层任何删除操作如果目标路径包含.git直接拒绝除非用户显式覆盖。def guard_git(path): parts os.path.realpath(path).split(os.sep) if .git in parts: raise PermissionError(禁止删除 .git 目录请先确认)这个规则看起来简单粗暴但非常有效。因为正常的工作流里没有任何理由让 Agent 去删.git。如果它想删一定是出了问题。另外我强烈建议所有用 Agent 做自动化的人把 Git 仓库的远程备份做成自动化的。比如每次 Agent 执行完任务后自动git push到一个私有远程仓库。这样即使本地.git被删历史还在。4. 如果你正在用 Claude Code 或类似工具现在该做什么4.1 检查你项目里的 Junction打开 PowerShell在你项目根目录执行Get-ChildItem -Recurse -Force -Directory | Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } | Select-Object FullName, Target这条命令会列出所有重解析点及其目标。如果你看到任何指向项目外部路径的 Junction就要特别小心。我自己的项目里就发现过几个一个是 Docker Desktop 创建的一个是 WSL 的挂载点还有一个是我自己之前做测试时留下的。这些在平时不影响开发但一旦 Agent 执行删除操作就是定时炸弹。4.2 给 Agent 配置安全模式Claude Code 本身有一些安全配置选项但默认值偏宽松。我建议至少做这几件事第一在项目根目录放一个.claudeignore或者类似的配置文件明确排除敏感目录。虽然这不是万能的但能挡住大部分误操作。第二把 Agent 的执行权限降级。不要用管理员权限运行 Agent用普通用户权限。这样即使它想删系统目录也会被权限拦截。第三开启操作日志。Claude Code 支持记录所有工具调用把这些日志保存下来出事后能追溯。4.3 建立删除前快照机制这是我从这次事故里学到的最实用的一招在任何批量删除操作前自动创建一个快照。在 Windows 上可以用robocopy做增量备份速度很快robocopy D:\projects\myapp D:\backups\myapp_%date:~0,4%%date:~5,2%%date:~8,2% /MIR /R:0 /W:0这个命令会在备份目录下创建一个镜像。/MIR表示镜像模式/R:0表示不重试/W:0表示不等待。对于几万个文件的项目通常几秒钟就能完成。如果你觉得每次删除都备份太重可以只备份.git目录和关键配置文件。.git目录通常不大但包含了全部历史是最值得保护的部分。4.4 重新审视你的 Agent 任务设计最后也是最根本的不要让 Agent 做清理这种模糊任务。清理临时文件这个指令对人类来说很明确但对 Agent 来说充满了歧义。什么是临时文件.tmp后缀的temp目录下的还是超过 7 天没访问的每一种理解都可能导致不同的删除范围。我的做法是把任务拆解成具体的、可验证的步骤第一步列出所有.tmp文件输出清单第二步人工确认清单第三步删除确认过的文件这样虽然慢一点但安全。Agent 的价值在于处理重复性工作不在于替你做决策。把决策权留在人手里把执行权交给 Agent这才是正确的分工。5. 这次事故对整个 Agent 生态的启示5.1 Agent 的能力边界需要重新定义当前 Agent 框架普遍在追求更强的自主性——能自己规划、自己执行、自己纠错。但这次事故说明自主性和安全性之间存在根本性张力。一个真正安全的 Agent应该在关键操作前停下来问一问。但停下来意味着中断流程意味着降低效率意味着用户体验变差。这是产品设计上的两难。我的判断是未来的 Agent 会分化成两类一类是高自主、低风险的比如写代码、查资料、做分析另一类是低自主、高风险的比如文件操作、系统配置、部署上线。后一类必须有人工确认环节这不是技术问题是产品定位问题。5.2 文件系统抽象层的缺失这次事故暴露了一个更深层的问题Agent 缺少一个理解文件系统物理结构的抽象层。现在的 Agent 看到的文件系统本质上还是 1970 年代 Unix 的那套抽象——目录树、文件、权限。但这套抽象没有表达链接、挂载、重解析点这些现代文件系统的核心概念。我认为未来的 Agent 框架需要引入一个文件系统感知层它能够区分逻辑路径和物理路径识别链接、Junction、挂载点理解删除操作的影响范围在操作前评估风险等级这个层不需要很复杂但必须有。否则类似的悲剧还会重演。5.3 给 Agent 开发者的具体建议如果你正在做 Agent 开发我建议在架构层面做这几件事第一工具层做安全封装。不要让 Agent 直接调用rm -rf或Remove-Item而是调用你自己封装的safe_delete。这个函数内部处理链接检测、路径校验、数量限制。第二引入影响范围预估。在执行任何破坏性操作前先计算它会影响到多少个文件、多大空间、哪些路径。如果超出阈值触发确认。第三记录完整的操作审计日志。包括操作时间、操作类型、目标路径、影响范围、执行结果。这些日志在出事后是唯一的追溯依据。第四做沙盒预演。对于高风险操作先在一个隔离环境里执行一遍观察结果再决定是否在真实环境执行。这在技术上完全可行成本也不高。6. 我自己的防护配置分享最后分享一下我目前在用的配置供参考。在 Claude Code 的项目配置里我加了这样一段{ permissions: { deny: [ Bash(rm -rf:*), Bash(Remove-Item -Recurse:*), Bash(rd /s:*), Bash(del /f /s /q:*) ], allow: [ Bash(git status:*), Bash(git diff:*), Bash(ls:*), Bash(dir:*) ] } }核心思路是禁止所有递归删除命令只允许查看类命令。如果 Agent 确实需要删除文件它必须用我提供的自定义工具那个工具内部有安全检查。另外我在项目根目录放了一个SAFETY.md里面写清楚了哪些目录不能碰、哪些操作需要确认。虽然 Agent 不一定会读但至少在我 review 它的操作时有个参照。还有一个小技巧我会定期用fsutil reparsepoint query扫描项目目录把所有 Junction 记录下来。如果发现新的、不是我创建的 Junction就立即排查来源。这个习惯帮我发现过几次工具自动创建的链接避免了潜在风险。说到底Agent 是个强大的工具但工具越强大使用者的责任就越大。103 秒删 4.8 万个文件这件事与其说是 AI 的问题不如说是我们这些做工具、用工具的人还没有建立起与之匹配的安全意识。这个坑值得每个做 Agent 的人认真踩一遍——当然是在别人的事故里踩不是在自己的生产环境里踩。