🤖HermesBlog
Hermes 官方教程 · 第29篇2026/8/9· Easy Understand Hermes Agent

安全深入——权限与隔离

Hermes Agent 官方教程第29篇:权限与隔离

安全深入:洋葱八层防线

安全深入——权限与隔离

聊到 Hermes Agent 的安全,很多小伙伴第一反应是“不就是装个 Docker 吗?”——其实远不止。官方文档里写得很明白,这是一套 纵深防御(defense-in-depth) 模型,整整八层防线,从“谁能跟你说话”一路管到“容器里能不能写文件”。今天咱们就一层层拆开看,重点聊聊权限审批和隔离这两块硬骨头。

八层防线,层层设卡

简单过一遍这八层,你就知道 Hermes 有多“惜命”了:

  1. 用户授权——谁能跟 Agent 对话(白名单、DM 配对)
  2. 危险命令审批——破坏性操作必须有人类点头
  3. 文件写入安全——write_file/patch 的黑名单和可选沙箱
  4. 容器隔离——Docker/Modal(以及 Singularity 插件)加固配置
  5. MCP 凭据过滤——子进程环境变量隔离
  6. 上下文文件扫描——检测项目文件里的提示注入
  7. 跨会话隔离——会话之间互不可见,cron 存储路径防目录穿越
  8. 输入净化——终端工具的工作目录参数做白名单校验,防 shell 注入

每一层单独看可能都有漏洞,但叠在一起,攻击者想穿透就得“过五关斩六将”。

危险命令审批:三种模式随你挑

核心配置在 ~/.hermes/config.yaml 里,approvals.mode 有三个档位:

approvals:
  mode: smart                     # smart | manual | off
  timeout: 300                    # 等用户回复的秒数
  cron_mode: deny                 # cron 任务撞上危险命令咋办
  single_query_mode: deny         # 单次查询(-q)会话撞上咋办
  unattended_mode: deny           # webhook/API 等无人值守会话撞上咋办
  mcp_reload_confirm: true        # /reload-mcp 前先问一声
  destructive_slash_confirm: true # /clear、/new 等破坏性命令先确认
  • smart(默认):让一个辅助 LLM 当“风险评估员”。python -c "print('hello')" 这种低风险命令直接放行;真危险的自动拒绝;拿不准的才弹窗问你。
  • manual:所有危险命令一律人工审批,一个都不放过。
  • off:全部跳过,等于裸奔。文档里特意加了警告——只建议在 CI/CD、容器这类可信环境里用。此时 trust: untrusted 的 MCP 服务器上的写入调用也不再弹信任提示;但 MCP 服务器声明的消费上限仍会询问。

YOLO 模式:作死开关,但有底线

YOLO 模式会绕过所有危险命令审批,激活方式有三种:

  1. 启动时加 hermes --yolo 或 hermes chat --yolo
  2. 会话里敲 /yolo 切换开关
  3. 设置环境变量 HERMES_YOLO_MODE=1

注意 /yolo 是开关,敲一次开,再敲一次关。开启后界面会有两个明显的视觉提醒:会话开头的红色横幅 ⚠ YOLO mode — all approval prompts bypassed,以及状态栏里的 ⚠ YOLO 标记,生怕你忘了自己在作死。

YOLO 在 CLI 和网关会话里都能用,但 /yolo 只作用于你敲它的那个会话。而且它会跟着会话一起存盘,重启也不丢:消息平台的聊天能扛过网关重启,TUI 和桌面 App 重新打开或恢复会话时也照样保留。不过在消息平台上,/new 和 /resume 开的新对话会把审批重新打开——想继续放飞得再敲一次。

但有个底线:硬性黑名单(hardline blocklist)永远拦着,YOLO 也绕不过去。也就是说,再怎么放飞,真正要命的命令还是会被摁住。

YOLO 和 approvals.mode: off 还会跳过 trust: untrusted MCP 服务器的信任提示:不可信服务器上的写入型工具会直接运行,作用范围按各来源的常规规则。/yolo 只管你敲它的那个聊天(别的聊天照样会问);--yolo 和 HERMES_YOLO_MODE 覆盖它们所作用的那个 Hermes 进程的会话;approvals.mode: off 覆盖该 profile 的所有聊天和 cron 任务。它们不会跳过:

  • MCP 服务器的消费上限:会让聊天超出已声明 spend_limit 的调用仍会询问,没人能回答时(cron 任务、hermes chat -q)直接拒绝。这是你设的预算,不是安全提示。
  • MCP 服务器发来的提问(elicitation):那是向你要输入,不是要权限。
  • 凭据保险库的支付卡填写提示:那是把卡交给页面;以及 MCP 事件订阅,那是安装常驻触发器。

审批超时与无头模式

几个值得注意的点:

  • timeout 默认 300 秒:审批请求发出后,用户 5 分钟没回应就超时。超时不算拒绝,也不会被“记仇”——想继续干,重新发条消息让它再跑一次就行,会弹一张新的审批卡,而“批准一次”只对那一次调用有效。
  • cron_mode、single_query_mode 和 unattended_mode:cron 任务、-q 单次查询、以及 webhook / msgraph_webhook / api_server 这类无人值守平台上的会话,都是“没人能回答审批”的场景,默认 deny——撞上危险命令直接拦下(不用干等整个超时),Agent 得另想办法;也可以设成 approve 全自动放行。有个例外:api_server 会话如果客户端能回答审批卡(/v1/runs 和流式 chat completions,通过 POST /v1/runs/{id}/approval),那它照样会收到审批请求。
  • destructive_slash_confirm:/clear、/new、/reset、/undo 这些毁会话的命令,现在会弹三选项对话框(批准一次 / 总是批准 / 取消)。在 Telegram、Discord、Slack 上走原生按钮,其他地方文字回退。TUI 里也认这个设置,HERMES_TUI_NO_CONFIRM=1 可以强制跳过。

内容级命令检查

危险命令检测器除了盯“破坏性动词”,现在还多认两种内容层面的形态:

  • 带密钥的 curl/wget 请求体:比如密钥命名的变量(-d "k=$OPENAI_API_KEY")、凭据文件(-F file=@.env、-T ~/.ssh/id_rsa、--post-file=/etc/passwd),或者把凭据文件管道喂给上传用的 curl/wget。注意,普通的 Authorization 请求头属于正常 API 用法,不会被标记。
  • 不可见或双向 Unicode 控制字符(零宽空格、从右到左覆盖、隔离符等):它们会让你“批准的命令”和“实际跑的命令”不是同一条。Emoji 的连接符序列不算。

这两种都跟其他危险模式一样,走正常的审批流程。另外提一句:早先版本在这里捆绑了外部 tirith 扫描器,现在已经移除——升级会顺手删掉 security.tirith_* 那套设置,也不会用别的东西顶上。

execute_code 脚本

execute_code 跑起来的脚本可以直接调 subprocess、os、shutil,这些调用根本不经过上面那些模式。所以消息平台、API 等网关会话会在脚本开跑前整脚本问一次;被拒或没人应答,脚本就不会执行。cron_mode、single_query_mode、unattended_mode 会像对待单条命令那样立刻决定脚本命运;YOLO 和 mode: off 则跳过询问。

本地会话(经典 CLI、TUI、桌面 App、ACP 编辑器)走的是交互式 CLI 的老规矩:在 manual(以及 smart)下,用户自己敲的那一轮不做整脚本审批——脚本里的 Python 级文件、进程操作直接跑,只有脚本里的 terminal() 调用会逐个弹审批,走会话自己的提示。这个豁免需要两个条件同时满足:这一轮是用户提交的,且脚本启动时有能回答审批的用户在线。以下情况在本地会话里仍然要整脚本审批:

  • 没人敲过的轮次:插件 inject_message、委派完成、后台进程和 watch 模式通知、/loop 心跳、托管房间任务
  • delegate_task 子任务和 /bg / /btw 旁支任务
  • 用户那一轮执行时没有能审批的用户在线

别直接改控制文件

有一点要特别提醒:不要让 Agent 直接去 patch ~/.hermes/cron/jobs.json 这类控制文件。正确姿势是用 cronjob_manage 工具、hermes cron 或 /cron——它们会通过受支持的 API 更新任务存储。其他 Hermes 控制文件也一样,写入安全机制会拦下直接编辑。

受监管网关的生命周期限制

终端工具还有一道独立且不可覆盖的护栏:禁止从网关自己的受监管进程内部去停止或重启网关。因为自我重启可能在工具执行完成前就把它干掉,进而引发“监管器 / 自动恢复”死循环。用户审批、YOLO 模式、force=True 都不能绕过这道护栏。

这道护栏还会拦下针对网关所跑解释器镜像的“杀进程”命令——taskkill /F /IM python.exe、Stop-Process -Name python、pkill -9 python3、killall python、pkill -f python,以及 pgrep python | xargs kill 这类按名字推导出来的杀法。原因很简单:受监管的网关本身就是个 python 进程,这么一杀连 Agent 自己这一轮都没了。只针对 Agent 自己拥有的进程的杀法可以放行:后台任务的 proc_* id(process(action="kill", …))或明确的 PID(taskkill /F /PID <pid>、kill <pid>)。其他镜像名(比如 taskkill /F /IM notepad.exe)不受影响。这道护栏在所有生成的启动器下都生效——systemd unit、launchd plist、s6 run script 和 Windows 计划任务,靠它们导出的 HERMES_SUPERVISED_CHILD 标记来识别。

在 macOS 上,实际执行的 launchctl submit 和 launchctl bootstrap 命令会被限制,不管 job label 是什么。这是一条保守的注册限制,目的是拦住那些用中性 label 的间接重启助手,而不是去检查目标 plist。它也会拒绝那些 RunAtLoad=false 且没有 KeepAlive 键的独立计划任务;被拒绝并不意味着该任务一定用了 KeepAlive 或控制了 Hermes。

如果确实需要维护 LaunchAgent,请在网关之外单独开一个 shell 操作。某些独立的 load/unload 命令目前能通过基于 label 的检查,但这并不是经过目标验证的豁免,也不是绕过 bootstrap 拒绝的受支持方式。只读的 launchctl print 不属于生命周期操作。外部维护之后,要把磁盘上的 plist 和已加载的 job 区分开:先校验 plist,再读回已加载的计划,然后才能报告“已激活”。

工具拒绝意味着该命令没有通过那次工具调用执行;助手自己决定不发这个调用,是另一回事(模型决策),换个模型也不会改变终端护栏的策略。

用户自定义拒绝规则(approvals.deny)

硬性黑名单是代码里写死的,approvals.deny 则是它的“用户可编辑版”:一串 glob 模式,匹配到的终端命令会被无条件拦截——在 --yolo、/yolo、approvals.mode: off 生效之前就拦下。想“除了这几件事,其他随便干”的话,就用它。

approvals:
  deny:
    - "git push --force*"
    - "*curl*|*sh*"
    - "dd if=* of=/dev/*"

几个细节:

  • 模式是 fnmatch glob(*、?、[...]),大小写不敏感,匹配整条命令文本,也匹配单个可执行命令候选。git push --force* 能匹配 git push --force origin main,但匹配不到 git push origin main。
  • 匹配跑在危险模式检测器用的同一套“规范化 / 去混淆”命令变体上,所以 git pu""sh --force 这种简单引号把戏躲不过去。
  • 可执行候选既保留字面路径,也匹配它的 basename:sudo * 覆盖 /usr/bin/sudo -n id 和 ./sudo -n id;而 /usr/bin/sudo * 这种路径专属规则不会变成对所有叫 sudo 的二进制都生效。
  • 引号感知的解析能揪出赋值、前置重定向、;、&&、||、管道、分组、命令替换,以及普通 if/then/else/do 转换之后的命令。支持的启动器包括 sudo、env、command、exec、nohup、setsid、time、nice、timeout、stdbuf、ionice、chrt、taskset、chroot。已知的选项操作数会被跳过;command -v/-V 查询不算执行。shell -c 载荷会被递归检查。env -S/--split-string 里的字面“可执行文件 + 参数”字符串按 GNU 引号和转义规则处理(包括 \_ 词边界和 \c 终止),其余命令参数追加在后面;这些参数里的 shell 标点保持为数据,除非真的被某个 shell -c 消费。env -a/--argv0 的值是参数,不是可执行文件名。shell 和 GNU split-string 的注释不会引入可执行候选。
  • 在额外的可执行候选中,词与词之间的空白会被折叠,但引号内的参数内容和参数路径会保留。所以像 git status 这样的精确规则,也能匹配 env git\tstatus; echo done(\t 代表制表符)。而 echo 'sudo -n id' 这种引号里的“提及”不会被提升为命令。已有的整条输入 glob(比如 *sudo*)仍然会有意匹配这类提及。
  • YAML 引号:模式一定要加引号。裸的开头 * 会被 YAML 当成别名解析失败,{、!、: 也各有含义,单引号对 shell 风格内容最安全。
  • 拒绝规则对所有终端后端都生效,包括隔离容器,且先于任何后端专属的审批捷径。被拦下的命令会给 Agent 返回一个 BLOCKED 错误,告诉它别重试、别换说法,什么都不会执行。
  • 和审批配置的其他部分一样,改完立即生效(配置缓存按 mtime 判断),不用重启会话。

沙箱镜像与基础设施密钥

如果你用容器沙箱,官方给的桌面沙箱镜像是:

docker_image: "nousresearch/hermes-sandbox:desktop"

另外有一条硬规矩要记住:Hermes 自己的基础设施密钥(provider API key、网关 token 之类)绝对不能塞进 env_passthrough。它们有专门的传递机制,不走这条路。你声明这类名字时会被直接拒绝;就算你先声明了、之后某个平台适配器(比如技能加载完之后才注册的插件适配器)把它认领了,从那一刻起它也会停止被转发。

TLS 证书信任

Hermes 通过 truststore 初始化平台验证器:Windows 走系统证书存储,macOS 走系统信任服务,Linux 走 OpenSSL 系统信任路径。初始化失败时会记日志并回退到 OpenSSL 默认值。

如果你在公司 TLS 代理后面,请按组织的操作系统信任流程安装其根证书。Hermes 的 provider 解析器不再通过 HERMES_CA_BUNDLE 或那套旧的 CA 环境变量阶梯来选择信任。沙箱子进程可以有自己的独立 CA 配置。原先的启动证书守卫也一并去掉了:Hermes 不再校验 HERMES_CA_BUNDLE / SSL_CERT_FILE / REQUESTS_CA_BUNDLE 这些变量。

小结

安全这玩意儿,从来不是单点防御,而是层层设防。Hermes 的八层模型把“人、机、环境”都考虑到了:人负责审批关键操作,机器负责扫描和隔离,环境负责容器和路径加固。对于普通用户,保持默认的 smart 模式就够用;真要开 YOLO,记得自己心里有点数。

📖 官方文档

本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/security