想让 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_rsaid_ed25519authorized_keys,以及 ~/.ssh/ 下除 config 之外的一切,全部保持“想都别想”。这一个例外都没有开。
  • tools/file_tools.pywrite_filepatch 在保护指令闸门之后,接上共享的 _run_approval_gate 审批闸门。
  • 非交互调用方 fail-closed:ACP 文件桥(agent/copilot_acp_client.py)直接拒绝需要审批的路径;TTS 输出路径选择器(tools/tts_tool.py)同样拒绝。

Hermes 文件安全的分层模型

这次改动顺手把 Hermes 的文件写入安全策略完整暴露出来了,一共三层,值得记住:

第一层:硬拒绝(hard-denied)——想都别想。 凭据和系统关键文件,任何工具、任何模式(包括 yolo)都写不了:~/.ssh/authorized_keysid_rsaid_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 里,这个共享闸门的判定顺序是:

  1. --yolo 优先绕过:yolo 模式(进程级 HERMES_YOLO_MODE 或会话级)直接放行——但注意,硬拒绝层在进闸门之前就拦下了,yolo 也越不过第一层;
  2. 会话缓存短路:之前批准过(session/always 粒度)的直接放行;
  3. 交互/网关/cron 分支:普通交互会话弹审批框;cron 会话按 approvals.cron_mode 决定(默认 deny 拒绝);
  4. 无人工通道时 fail-closed:既不是交互终端也不是网关会话(比如后台脚本、ACP 调用),直接返回 BLOCKED——绝不替你做主。拒绝信息还会提醒 agent“不要绕过审批用 terminal 或 execute_code 重试,除非用户明确同意”。

小结

~/.ssh/config 从“硬拒绝”变成“审批门控”,本质是把规则从“工具决定”拉回“你决定”:常规编辑不再被一刀切挡死,而带执行指令的写入也始终有一道人工闸门。想深入了解 Hermes 的审批机制,可以看智能审批三闸门指南审批疲劳的解法--yolo 与红线的关系见这篇文章;相关配置命令参考在 hermes-config 页面。