幕后

AI 帮我停个开发服务,顺手把我的浏览器全杀了——两次

POMODIARY 团队 · · 阅读约 2 分钟

9 月 14 号,我的 Chrome 所有标签页同时崩了。

Claude 桌面端、ChatGPT、Obsidian 也一样,同一秒钟全部白屏。

我盯着屏幕愣了几秒,然后意识到:这个场面我见过。一周前,9 月 7 号,一模一样。

凶手是一行看起来很谨慎的命令

两次的起因都一样:我让 AI 帮我把本地的 Next.js 开发服务停掉。它跑了这么一行:

pkill -f "next-server" -U "$(id -u)"

单看这行命令,你甚至会觉得它挺周到的:-f 按完整命令行匹配 next-server,-U 再限定「只杀当前用户的进程」,双保险。

问题就出在这个「双保险」上。

macOS 的 pkill 不会帮你重排参数

Linux 上的 pkill 用的是 GNU getopt,参数顺序随便写,它会自己整理好:选项归选项,pattern 归 pattern。

macOS 上的 pkill 用的是 BSD getopt:碰到第一个不是选项的参数,就停止解析,后面的东西一律当成 pattern。而 pkill 又支持同时传多个 pattern,命中任意一个就算数。

所以这行命令在 macOS 眼里其实是三个 pattern:

  • next-server
  • -U
  • 501(我的用户 ID)

再加上 -f 是按完整命令行匹配,它的真实含义变成了:杀掉所有命令行里带 -U 或 501 的进程。

Chrome、Claude、ChatGPT、Obsidian 这类基于 Chromium / Electron 的应用,每个标签页、每个窗口背后都是一堆 Helper 进程,启动参数又长又杂,非常容易命中。一行命令下去,全部阵亡。

本来想加一层保险,结果这层保险本身成了炸弹。

我后来怎么改的

第一,停开发服务按端口找,不按名字猜。

lsof -ti :3000 -sTCP:LISTEN | xargs kill

谁在监听这个端口就杀谁,不存在误伤。

第二,非用 pkill 不可的话,选项全部写在 pattern 前面,pattern 具体到端口或路径。 动手之前,先用 pgrep 带同样的参数预览一遍:

pgrep -fl "next start -p 3011"
pkill -f "next start -p 3011"

先看会命中谁,再动手。

第三,也是我觉得最重要的一条:把它写成钩子,别只写成规则。

规则是请求,钩子才是保证

同一个坑我踩了两次。现在我给 Claude Code 的全局规则文件 CLAUDE.md 里专门有一节讲停进程,两次出事的日期 9 月 7 号和 9 月 14 号都记在里面。

第二次之后,我不再指望「记住」这件事了。我用 Claude Code 的 PreToolUse 钩子写了个两百多行的 Python 脚本:AI 每次要执行 Bash 命令之前先过一遍检查,只要发现 pkill / pgrep 把选项写在了 pattern 后面,直接拦下来,不让执行。

这件事让我想明白一个区别:

写在提示词、规则文件里的东西,对 AI 来说是「请求」。它大概率会遵守,但上下文一长、任务一急,就有漏掉的可能。

写在钩子里的东西是「保证」。它不依赖 AI 记不记得、理不理解,命令根本到不了 shell。

管理团队其实也是一个道理:重要的事情不要靠「大家注意一下」,要靠流程卡住。能用 CI 拦的别靠 code review,能用权限拦的别靠自觉。

现在让 AI 直接在自己电脑上跑命令已经是日常了。它犯的错不见得比人更离谱,但它犯错的速度比人快得多。给它立规矩的同时,记得装护栏。

至于我,现在每次看到 pkill,都会条件反射地先看一眼参数顺序。

两次全军覆没的标签页,换来这么一个肌肉记忆,也不算太亏。

继续阅读