用 AI 编程助手(Claude Code、ZCode 这类 agent)在 Windows 上干活的人,迟早会在某个项目根目录捡到一个幽灵:一个 0 字节的文件,名字叫 nul。git status 里躺着个 ?? nul,资源管理器里看得见,右键删除却报"找不到该项目"。看得见,删不掉,标准的幽灵文件。
它不是病毒,不是 bug,是 AI 助手的一句跨平台口误。
口误发生在哪一句
AI 助手在 Windows 上跑命令,走的通常是 Git Bash——一个 POSIX 语义的 shell。而模型执行命令时有个职业习惯:把无关的日志和报错重定向丢弃,保持输出干净。丢弃输出的写法在两个世界长得不一样:
Windows cmd: some.exe >nul (nul 是设备,输出进了黑洞)
Bash: some.exe >/dev/null (/dev/null 是设备,输出进了黑洞)
模型识别到宿主是 Windows,出于好意切了 cmd 方言,写下 >nul。但实际执行这句的 shell 是 Git Bash。在 Bash 眼里,nul 不是设备,就是一个普通的文件名——重定向的目标文件不存在时 Bash 会怎么办?创建它。于是磁盘上多了这么个东西。
一句字面级别的口误:语法是 cmd 的,解释器是 Bash 的。两边单独看都没错,拼在一起就生成了一个不属于任何一方意图的文件。
为什么看得见、删不掉
nul 是 DOS 时代的设备保留名,同一家族还有 con、prn、aux、com1 到 com9、lpt1 到 lpt9。Windows 的 Win32 层在解析路径时,会把出现在文件名位置上的这些名字别名到设备对象上。所以资源管理器、del nul、以及一切走 Win32 路径的程序,操作这个名字时碰到的其实是设备,不是文件——“找不到该项目"就是这么来的:它找的确实不是那个文件。
那 Git Bash 为什么建得出来、又删得掉?因为 MSYS2/Cygwin 这套环境走的是 NT 原生路径,绕过了 Win32 层的路径规范化,保留名的别名机制对它不生效。同一个名字,两层 API 给出两种身份:Win32 说它是设备,NT 说它是文件。幽灵的实质就是这层身份分裂。
这个别名发生的位置,用 .NET 直接删一次就能看到现场:把 D:\...\nul 传进去,抛回来的异常里路径已经变成了 \\.\nul——访问被拒,因为你试图删的是一个设备。
删掉它,不需要粉碎机
网上不少文章教人拿安全软件的"文件粉碎"对付它,其实用不上。按顺手程度排:
- 就在 Git Bash 里删:
rm -f nul。既然它建得出,就删得掉,同一个道理。 - VS Code 里右键删除。在资源管理器侧栏选中它,按 Delete 就没了。这不是玄学:VS Code 的文件操作建立在 Node.js / libuv 这一层,路径在库层就被转成按字面处理的形式,Win32 的保留名别名拦不到它——实测 Node 用裸路径
fs.unlinkSync就能直接删掉。
至于老资料里的祖传偏方 del \\?\D:\path\to\nul:原理是真的(\\?\ 前缀让系统跳过路径规范化、按字面处理,保留名检查随之失效),但至少在我这台 Windows 11(build 22631)上,cmd 的 del 已经不吃这套,直接报"文件名、目录名或卷标语法不正确”,PowerShell 同样不认这个前缀。逃生门还留在 API 层,只是门前的路没了——好在有 Git Bash 和 VS Code,你也不需要它。
让它别再发生
删文件是治标,治本在提示层。AI 助手是读上下文的,在项目的 AGENTS.md / CLAUDE.md 里写一句环境声明,一劳永逸:
本机 shell 是 Git Bash(POSIX 语义)。丢弃输出用 >/dev/null,禁止 >nul。
这里有个值得玩味的细节:口误的根源恰恰是模型的"礼貌"——它看到宿主是 Windows,主动切换了方言,却不知道执行环境其实是 Bash。**好心切换方言的前提是知道对端是谁,而 agent 对运行环境的认知,默认是猜的。**所以环境要显式声明,不能靠它推断。
顺带一提,这个幽灵还有个副作用挺有意思:git status 里躺着一个 nul,等于留下了一份物证——某次会话里,agent 曾以为自己在跟 cmd 说话。排查 agent 的行为时,别急着怪模型,先看看物证。
写在最后
幽灵本身不难缠,一句 rm -f nul 就送走。难缠的是它背后那个"我以为我在哪"的误会:模型好心切换方言,shell 按字面执行,两层系统各自都正确,合起来却生成一个谁也不想要的文件。给 agent 一句明确的环境声明,比事后收拾残局省事得多。
备注:本文参考了什么值得买上的一篇旧文,该文把现象和 >nul 的成因讲清楚了;本文在其基础上补全了 Win32 / NT 双层 API 的机制解释、实测可用的删除方法和提示层的预防方案。文中删除方法均在 Windows 11(build 22631)+ Git Bash 环境实测。