Hermes 现在会拦下包管理器卸载:卸载包之前,先过危险命令审批


一个全局 npm 包让构建莫名其妙变慢,你顺着线索找到了它,随手敲下 npm uninstall -g some-package,回车之前也没多想。两天后你发现 CI 里少了它,而这台开发机上的所有项目都跟着受影响。装上一个包很容易被注意到,删掉一个包却可能让你排查半天——“删除”比“安装”危险得多。可过去当 Hermes Agent 替你动手的时候,这条命令根本不会触发任何提示:危险命令审批只认识 rm -rf 这一类 shell 原语。PR #109170 补上的就是这个缺口。

安装很少疼,删除才疼

先把机制说清楚:危险命令审批是 Hermes 内置的一组规则词库。当 agent 准备执行一条后果无法撤销的命令时,它会先弹出一个审批提示等你确认,而不是直接跑下去。这套规则的设计出发点很朴素——别让一次不可逆的操作悄悄溜过去。

而“安装”和“卸载”在这件事上并不对称。装一个包是加法:多出来的东西你很快就会注意到,出了问题删掉就行。删一个包是减法,而且往往是共享的减法——一个全局包被卸掉,这台机器上所有依赖它的项目会一起受影响,而没有任何东西告诉你“这里刚刚发生了什么”。

这也是这次改动只拦删除、不拦安装的原因。npm update -g、pip install、brew upgrade、yarn install 依旧不会被标记,也不会弹提示。如果把装包也一并拦下,审批条会变成每天要点十几次的噪音,没人再认真读它——那比不拦更糟。

以前的规则表只认识 shell 原语

问题出在 tools/approval_detection.py 的 DANGEROUS_PATTERNS 这张模式表上。它拦的是 shell 原语——rm -rf、git reset --hard 这一类彻底、广为人知、一眼就能看出破坏力的命令。但表里没有任何一条“包管理器删除动词”的规则。

所以 issue #10199 里那次事故真正发生时,命令一条提示都没有,就那么跑完了。不是规则判断错了,是这条命令压根没被规则覆盖。

现在会被拦住的五种删除写法

PR #109170 的做法很直接:在 DANGEROUS_PATTERNS 里为每个包管理器补一条 "package manager uninstall" 规则。整条 PR 的改动量是 +34 / −0,只碰了两个文件:tools/approval_detection.py 和 tests/tools/test_approval.py。

也就是说,下面这些命令从今以后都会走危险命令审批:

  • npm —— npm uninstall -g some-package。删除动词覆盖 uninstall / unlink / remove / rm / r / un
  • pnpm —— pnpm remove。删除动词覆盖 uninstall / remove / rm / un
  • yarn —— yarn remove。规则允许前面带一个可选的 global,后面跟 uninstall / remove
  • pip / pip3 —— pip uninstall。删除动词就是 uninstall
  • brew —— brew uninstall。删除动词覆盖 uninstall / remove / rm

注意规则管的不是“某个具体包名”,而是每个管理器的删除动词本身——所以 uninstall 后面跟什么、跟几个,都不影响它被拦下。

两个容易被误伤的地方,他们想到了

给规则表加词最容易犯的错就是加得太宽,把正常命令一起拦了。这次有两处细节值得一提。

第一,规则用 _CMDPOS 锚定在命令位置。 和这个文件里其他“命令名规则”一样,模式必须出现在命令开头的位置才算命中。所以引号里的文字不会被误判——比如 git commit -m "document npm uninstall usage" 只是一条提交信息,不会弹审批。

第二,每个全局选项允许带一个非破折号参数。 也就是 npm --prefix DIR uninstall x、pip --proxy URL uninstall x、yarn --cwd DIR remove x 这类写法照样会被拦住。选项后面跟一个参数是极其常见的用法,漏掉它,规则就形同虚设。

测试也跟上了:tests/tools/test_approval.py 新增了 6 个参数化用例,把六种删除写法归到同一个 key 上。这些用例在旧代码上是红的;修完之后 129 个用例全绿,tests/tools/ 整个目录 9141 个用例也是绿的。

这对你意味着什么

如果你的 Hermes 会话旁边有人看着,这件事很简单:agent 要卸载包的时候会停下来问你,你确认或者拒绝。多一次点击,换来的是“删掉的东西确实是你打算删的”。

真正需要提前想清楚的是无人值守的场景——cron 任务、后台跑的 agent。那里没有人能回答审批提示,一条本该被拦下的卸载命令会停在半路。如果你有自动化流程确实需要卸载包,别指望它自己过去:先用 hermes approvals test 把命令对着真实守卫预演一遍,看清它会落在哪一档,再决定是放行、改造流程,还是干脆不让 agent 碰这一步。cron 场景下审批会怎么表现,无人值守审批指南里有更完整的讨论。

还有一件事要分清楚:危险命令审批与 approvals.deny 是两套不同的机制。前者是内置规则词库,你不需要写任何配置它就会生效;后者是你自己维护的预定义拒绝列表。这篇只讲前者,后者的写法可以看智能审批配置,这里不重复。

最后把发布状态说清楚:这个改动是 2026-09-12 合入 main 的,晚于 v0.21.2(2026-09-11 发布)。所以它还没有出现在任何正式发版里——已经进了主线,但要等下一个版本才会到你手上。想先确认自己当前版本的审批行为,上面那条 hermes approvals test 就是最直接的答案;v0.21.2 发版说明是它之前最近的一次正式发布。

小结

  • 包管理器的卸载命令现在会走危险命令审批:npm uninstall -g、pnpm remove、yarn remove、pip uninstall、brew uninstall 都在内。
  • 安装和更新仍然不弹提示:npm update -g、pip install、brew upgrade、yarn install 不受影响。审批留给真正不可逆的操作。
  • 规则锚定在命令位置,引号里的 npm uninstall 字样不会误伤;全局选项后面带一个参数也不会让规则失效。
  • 改动已合入 main,尚未进入正式发版。