想让 Hermes 帮你改 SSH 配置?现在它会先征求你的同意

你想让 Hermes 帮你把新服务器加进 SSH 配置——加上 host 别名、ProxyJump 跳板机,这样以后 ssh work 一行就能连上。结果呢?write_file 直接甩你一句 “Write denied: ~/.ssh/config is a protected system/credential file”,像是踩了红线;可你转头用 terminal 敲一句 printf 写同一个文件,它却只是弹个确认框,你点一下“批准”就成功了。同一个操作,两个工具,两套规则——到底哪个才是对的?8 月 12 日合并的 PR #84663 把这个问题修掉了:SSH 客户端配置不再是“想都别想”的禁区,而是改成了“先问过你”的审批门控。
问题根源:同一个文件,两条互相矛盾的规则
矛盾来自两套独立的检查逻辑:
write_file/patch工具走的是agent/file_safety.py里的硬拒绝清单(deny list)。以前~/.ssh/config同时命中精确路径拒绝和~/.ssh/目录前缀拒绝,所以直接报“受保护文件”,没有任何商量余地。terminal工具走的是tools/approval.py的危险命令审批。往~/.ssh写内容只会被标记为“需要审批”,你批准后就能写进去。
于是同一个 ~/.ssh/config,用文件工具写是“死路”,用终端写是“问一下就行”。这种反复横跳不仅让用户困惑——agent 也会先报告“写入失败”、再用另一条路“成功”,日志里留下一堆自相矛盾的记录。
修复:~/.ssh/config 从硬拒绝降级为审批门控
PR #84663 的思路很清晰:SSH 客户端配置不是凭据。它里面不存私钥字节,编辑它(加 host 别名、配 ProxyJump、配 VS Code Remote-SSH 目标)是开发者天天要做的常规操作,一刀切的拒绝是错的。但它也确实可能携带 ProxyCommand / Match exec 这类会执行命令的指令,所以静默放行同样危险。结论是:审批才是正确的策略——和 terminal 工具对 ~/.ssh 写入的既有行为对齐。
具体改动如下:
agent/file_safety.py:把~/.ssh/config从 flat credential deny 清单里拿掉;新增build_write_approval_paths()和is_write_approval_required(),在_classify_write_denial里把审批门控路径从~/.ssh/前缀拒绝中短路出来。- 私钥和授权文件仍然硬拒绝:
id_rsa、id_ed25519、authorized_keys,以及~/.ssh/下除 config 之外的一切,全部保持“想都别想”。这一个例外都没有开。 tools/file_tools.py:write_file和patch在保护指令闸门之后,接上共享的_run_approval_gate审批闸门。- 非交互调用方 fail-closed:ACP 文件桥(
agent/copilot_acp_client.py)直接拒绝需要审批的路径;TTS 输出路径选择器(tools/tts_tool.py)同样拒绝。
Hermes 文件安全的分层模型
这次改动顺手把 Hermes 的文件写入安全策略完整暴露出来了,一共三层,值得记住:
第一层:硬拒绝(hard-denied)——想都别想。 凭据和系统关键文件,任何工具、任何模式(包括 yolo)都写不了:~/.ssh/authorized_keys、id_rsa、id_ed25519、.env、.anthropic_oauth.json、.netrc、.pgpass、.npmrc、.pypirc、.git-credentials、/etc/sudoers、/etc/passwd、/etc/shadow,以及 ~/.ssh/、~/.aws/、~/.gnupg/、~/.kube/、~/.docker/、~/.config/gh/ 等目录前缀。这一层是底线。
第二层:审批门控(approval-gated)——先问过你。 目前只有 ~/.ssh/config 一个成员。可写,但必须过人工审批,因为它能夹带会执行命令的指令。审批通过后,门控还支持 once(仅这一次)、session(本次会话内记住)、always(永久记住)三种粒度。
第三层:自由写入(free write)——其余所有文件。 正常的工作文件、项目代码,agent 随便写。
审批门控的完整行为
在 tools/approval.py 的 _run_approval_gate 里,这个共享闸门的判定顺序是:
--yolo优先绕过:yolo 模式(进程级HERMES_YOLO_MODE或会话级)直接放行——但注意,硬拒绝层在进闸门之前就拦下了,yolo 也越不过第一层;- 会话缓存短路:之前批准过(session/always 粒度)的直接放行;
- 交互/网关/cron 分支:普通交互会话弹审批框;cron 会话按
approvals.cron_mode决定(默认 deny 拒绝); - 无人工通道时 fail-closed:既不是交互终端也不是网关会话(比如后台脚本、ACP 调用),直接返回 BLOCKED——绝不替你做主。拒绝信息还会提醒 agent“不要绕过审批用 terminal 或 execute_code 重试,除非用户明确同意”。
小结
~/.ssh/config 从“硬拒绝”变成“审批门控”,本质是把规则从“工具决定”拉回“你决定”:常规编辑不再被一刀切挡死,而带执行指令的写入也始终有一道人工闸门。想深入了解 Hermes 的审批机制,可以看智能审批三闸门指南和审批疲劳的解法;--yolo 与红线的关系见这篇文章;相关配置命令参考在 hermes-config 页面。