AI 清理进程为何会误关 Mac 应用
文章整理 Claude Code 的进程误杀报告,指出 pkill -f 会用正则匹配完整命令行,短模式可能命中大量无关路径。案例展示了自动清理卡住进程时,匹配范围失控造成的影响。
AI 深度解读
让 AI 停掉一个卡住的 cat,结果 Safari、Chrome、Docker Desktop 连同 Claude 桌面端一起退出。原文讨论的不是模型突然发疯,而是一条看起来加了限制的命令,如何同时踩中进程匹配和 macOS 参数解析的坑。
作者的结论很实用:终止进程时,优先明确 PID;要约束编程代理,就把检查放到命令执行之前。不过,文中的最小拦截脚本和作者维护的工具,能力边界并不相同。
为什么只想停掉 cat,却关掉了一批应用
Claude Code 仓库的 #100684、#107867 报告了类似事故:代理执行形如 pkill -f 'cat' -n -u 501 2>/dev/null 的命令后,多个桌面应用退出。统一日志显示,几十个 launchd 服务在同一秒收到由 pkill 发出的 SIGTERM。#100684 后续还有版本 2.1.284、2.1.309、2.1.318 的复现反馈。
第一个问题是 -f。
不带它时,pkill cat 按进程名匹配;带上后,匹配对象变成完整命令行,而且使用正则表达式。cat 不再表示「那个叫 cat 的程序」,任何命令行里出现这三个连续字母的进程都可能命中。
macOS 应用常见的路径是 /Applications/Safari.app/Contents/MacOS/Safari,其中 Applications 就包含 cat。Library/Caches、com.apple.location...、CoreLocationAgent 也可能撞上。作者在自己的 MacBook 上用 pgrep -lf cat 检查,发现命中了 105 个位于 .app 包内的进程。
这也不是 cat 独有的问题。#90070 中,代理想重启一个开发服务器,执行 pkill -f "next-server",顺带关闭了另一个项目的服务。对同时开着多个 Next.js 项目的开发者来说,这个事故更容易遇到。
加了 -n 和 -u,为什么还是没限制住
第二个问题更隐蔽:参数位置。
原命令里的 -n 原本用于只选最新匹配项,-u 501 原本用于限定用户。但作者指出,macOS 自带的 BSD pkill 遇到第一个非选项参数后,就停止解析选项。放在 cat 后面的内容被当成额外匹配模式,没有起到预期限制作用。
作者用接收同类参数的 pgrep 做了不发送终止信号的对照:
$ pgrep -f 'cat' -n -u $(id -u) | wc -l
180
$ pgrep -n -u $(id -u) -f 'cat' | wc -l
1
要看的是第二行命令:把选项全部放到模式之前,作者机器上的匹配数量从 180 变成了 1。这组结果是在 macOS 27.2 上实测的,不代表每台机器都会命中相同数量。
Linux 的 procps pkill 会按 GNU 风格重排参数,因此第一种写法在那里可以表达「只选最新匹配项」。作者推测,模型接触了大量 Linux 示例,可能把这种顺序带到了 macOS;但原文没有提供训练数据分析,这只是解释事故的一种假设。
命令末尾的 2>/dev/null 还会隐藏标准错误,使用 ; 串接命令则会让后续命令继续执行。两者叠加,代理更难从执行反馈里察觉异常。
限制条件写进了命令,不代表系统真的按限制条件执行。
更可靠的办法,是明确目标并在执行前检查
作者首先建议按 PID 终止进程。代理自己启动的后台任务,可以从 $! 取得 PID;按端口寻找开发服务,可以用 lsof -ti tcp:3000 -sTCP:LISTEN。
只有名称可用时,pkill -x cat 比完整命令行的子串匹配更明确。作者还给出 pkill -n -P $$ -x cat:所有选项都放在模式之前,并结合当前 shell 的父子进程关系缩小范围。执行前先用 pgrep -lf '<pattern>' 查看匹配对象,也能暴露误伤范围。
但这些建议仍依赖代理每次都记得遵守。作者因此提供了一个 Node.js PreToolUse hook,在 Claude Code 的 Bash 命令进入 shell 前检查它:识别 pkill -f,调用 execFileSync 执行 pgrep,如果匹配结果包含 .app/Contents/,就通过 hookSpecificOutput 返回 permissionDecision: 'deny'。
作者称,针对三个 issue 中的原始命令测试时,cat 变体被拒绝,pkill -f my-worker.js 和普通 pkill cat 可以通过。不过,这个最小脚本不处理 sh -c '...'、xargs 或 killall。
作者维护的 claude-bulkhead 则进一步考虑多个 Claude Code、Codex、Gemini CLI 会话。旧规则能保护其他代理会话的进程,却漏掉了「只有一个会话,也误伤桌面应用」的情况。
0.3.5 新增规则:pkill -f <pattern> 命中 .app 包内进程,或者命中会话自身进程树之外至少 8 个进程,就会被阻止。会话自己启动的进程不计入后一个阈值。
下面这条命令用于查看工具如何解释事故命令:
npx claude-bulkhead explain -- "pkill -f 'cat' -n -u 501"
要看的是 -- 后面的字符串,它是被分析的命令。作者还称,完整解析器覆盖 bash -c、ssh localhost、传给 shell 的 heredoc、xargs,并能区分仅出现在引号文本中、实际不会执行的终止命令。项目采用 MIT 许可、没有依赖,也不向机器外发送数据。
我的判断
最有价值的部分,是把故障拆成了两个可以分别验证的问题:匹配范围过宽,以及 BSD 参数顺序。前者解释为什么会碰到桌面应用,后者解释为什么看似存在的限制没有生效。
把保护放在执行前也合理。对经常执行 shell 命令的编程代理,规则检查比反复提醒「小心一点」更容易形成稳定约束。
但原文没有给出最小脚本或 claude-bulkhead 的系统性漏报、误报数据,8 个进程这个阈值也没有对照实验支撑。它是作者采用的工程规则,适合当作可审查的方案,不能直接当成安全保证。
按 PID 处理通常更直观,但仍要确认 PID 对应谁;按 3000 端口找到监听者,也不自动证明它属于当前项目。精确名称匹配解决的是名称误匹配,进程归属还得单独判断。
另外,原文称 npx 检查无需安装或运行任何东西,表述并不严谨:这条命令仍会运行工具,环境中没有对应包时还可能下载它。准确的理解应是「查看判定结果,无需真的执行那条终止命令」。
能拿来做什么
- 给 AI 编程约定服务启动方式时,同时保留后台 PID 或进程句柄,让后续停止操作能找到原来的任务。
- 同时运行多个 Next.js 项目时,要求代理先核对端口、完整命令行和进程归属,再停止具体进程。
- 把 macOS 上的终止命令统一写成「选项在前、模式在后」,并在执行名称匹配前展示
pgrep结果。 - 接入
PreToolUse防护时,先用团队真实命令检查判定结果,尤其覆盖包装在 shell、xargs中的命令,再决定规则范围。