安全设置——放心让 AI 帮你干活
Hermes Agent 官方教程第九篇:安全。命令审批、文件保护、容器隔离。
安全设置——放心让 AI 帮你干活
咱们写代码、跑命令的时候,最怕啥?怕 AI 一个手滑,把咱辛辛苦苦写的文件给删了,或者执行了什么危险的系统命令。别担心,Hermes Agent 在安全这块儿,那是下了血本的。今天咱就用人话聊聊,它到底是怎么给你“上保险”的。
八道防线,层层设卡
Hermes 的安全模型,你可以理解成进小区要过八道门禁:
- 用户授权:不是谁都能来使唤你的 Agent,得在“白名单”里才行。
- 危险命令审批:这是最重要的一道关卡,咱们下面细说。
- 文件写入保护:给
write_file这类操作加了个“黑名单”和“沙箱”,不让它乱写乱改。 - 容器隔离:把 Agent 关在 Docker、Modal(以及 Singularity 插件)的“小盒子”里跑,就算它“造反”,也砸不坏你的主系统。
- MCP 凭据过滤:给 MCP 子进程单独隔离环境变量,防止你的 API 密钥被偷看。
- 上下文文件扫描:会自动检查项目文件里有没有“提示词注入”的恶意内容。
- 跨会话隔离:每个会话的数据都是独立的,谁也不能偷看谁的。定时任务存储路径也做了加固,防止被“路径穿越”攻击。
- 输入净化:对终端工具的目录参数进行校验,防止通过构造特殊路径来搞“shell 注入”。
核心机制:危险命令审批
Hermes 在执行每条命令前,都会拿它跟一个“危险行为清单”比对。一旦命中,就得你亲自点头,它才敢动手。
这个审批有三种模式,在 ~/.hermes/config.yaml 文件里配置:
approvals:
mode: smart # smart | manual | off
timeout: 300 # 等你审批的超时时间(秒)
cron_mode: deny # 定时任务遇到危险命令时咋办
single_query_mode: deny # 单次查询(-q)遇到危险命令时咋办
unattended_mode: deny # webhook/API 等无人值守会话遇到危险命令时咋办
mcp_reload_confirm: true # 重载 MCP 工具前是否询问
destructive_slash_confirm: true # 执行 /clear 等破坏性命令前是否询问
- smart(智能模式,默认):它会先让一个“小助理”AI 评估风险。像
python -c "print('hello')"这种人畜无害的命令,直接就放行了;真正危险的,直接拒绝;拿不准的,才来问你。 - manual(手动模式):甭管危不危险,只要是清单里的,统统先问你一遍再说。
- off(关闭模式):所有检查全部关闭,相当于“裸奔”。所有命令都不再弹审批,
trust: untrusted的 MCP 服务器上的写入调用也不再弹信任提示(但 MCP 服务器声明的消费上限仍然会问)。强烈不建议在正式环境这么干,除非你在 CI/CD 容器里。
顺带提一句:审批提示超时之后是没法“续杯”的——那条待审批记录会被丢掉,Agent 也会被告知这一轮别自己重试。想跑的话,重新发条消息让它再来一次就行(比如“现在跑吧”),它会发起一次新的工具调用,弹出一张新的审批卡片;而“只批一次”只对那一次调用有效。超时不算拒绝,所以再问一次完全没问题。
内容级命令检查
危险命令检测器除了盯“破坏性动词”,还会拦两种内容层面的形态:
- 带密钥的
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、桌面应用、ACP 编辑器)走的是交互式 CLI 的规则。在 manual(以及 smart)下,由会话查看者亲手输入的那一轮不做整脚本审批:脚本里 Python 层面的文件和进程操作直接跑,只有脚本里的 terminal() 调用会按会话自己的提示来问,跟单网关运行时之前的 CLI 一样。这个豁免需要同时满足:这一轮是查看者提交的,且脚本启动时有能应答审批的查看者在线。以下这些在本地会话里仍然保留整脚本审批:
- 没人亲手输入的那些轮次:插件的
inject_message轮次、委派完成、后台进程和监视模式通知、/loop心跳、hosted-room 任务 delegate_task子任务和/bg//btw旁支任务- 查看者的一轮在跑,但当时没有能审批的查看者在线
有个“一键放飞”的功能叫 YOLO
想彻底放飞自我?可以用 YOLO 模式。它会跳过所有危险命令的审批提示。有三种开启方式:
- 启动时加参数:
hermes --yolo - 对话里输入:
/yolo(这是个开关,再输一次就关掉) - 设置环境变量:
HERMES_YOLO_MODE=1
开启后,会话顶部会有一条红色的 ⚠ YOLO mode 警告横幅,状态栏里也会一直有个 ⚠ YOLO 的小标志,时刻提醒你:“哥们儿,现在没人管你了啊!”
YOLO 在 CLI 和网关会话里都能用。/yolo 只对它被输入的那个会话生效,而且会跟着会话一起保存,所以重启也还在:消息平台的聊天能扛过网关重启,TUI 和桌面应用在你重新打开或恢复会话时也保留。不过在消息平台上,/new 和 /resume 开的新对话,审批会重新打开。
注意:YOLO 模式也不是啥都不管,它依然会拦截那个“硬性黑名单”里的命令。但除此之外,它真的是“神挡杀神,佛挡杀佛”了。建议只在完全可信的环境里用,比如一次性测试容器。
YOLO 和 approvals.mode: off 还会跳过 trust: untrusted MCP 服务器的信任提示:不受信服务器上的可写工具会直接运行,作用范围跟各来源平时一样。/yolo 只管它被输入的那个聊天(别的聊天照样会问);--yolo 和 HERMES_YOLO_MODE 覆盖它们被赋予的那个 Hermes 进程的所有会话;approvals.mode: off 覆盖该 profile 下的所有聊天和定时任务。它们不会跳过:
- MCP 服务器的消费上限:会让聊天超出已声明
spend_limit的调用仍然会问,而在没人能应答的地方(定时任务、hermes chat -q)会被拒绝。这个上限是你设的预算,不是安全提示。 - MCP 服务器发来的提问(elicitation):那是向你要输入,不是要权限。
- 凭据保险库的支付卡填写提示:它会把卡交给页面;以及 MCP 事件订阅,它会装一个常驻触发器。
硬性黑名单:YOLO 也管不了的那条底线
有些命令实在太“作死”了——比如把根目录一锅端、fork 炸弹、直接往块设备上写数据——Hermes 会无条件拒绝执行,不管你有没有开 --yolo、有没有把 approvals.mode 设成 off、是不是在定时任务里点了“总是允许”。这条黑名单是 --yolo 下面的地板,在审批层看到命令之前就先拦下来了,也没有任何开关能绕过。目前覆盖的模式包括:
| 模式 | 为什么是硬性拦截 |
|---|---|
rm -rf / 及其明显变体 |
直接抹掉文件系统根目录 |
rm -rf --no-preserve-root / |
就是那句“对,我就是要删根目录” |
:(){ :|:& };:(bash fork 炸弹) |
把主机 CPU 占满直到重启 |
对已挂载的根设备执行 mkfs.* |
格式化正在运行的系统 |
dd if=/dev/zero of=/dev/sd* |
把物理磁盘清零 |
在根文件系统顶层把不可信 URL 管道给 sh |
远程代码执行攻击面太广,没法批 |
另外,如果一条命令的 shell 引号压根解析不了(比如 grep 'unterminated),这条底线也会“宁可错杀”——直接报一个 malformed executable payload 的错误。判断是按你写的原文来的,所以引号里合法的转义(像 grep -o "[^\"]*" file)不会被误判,分隔符前面那种转义引号(echo "a\"b"; reboot)也藏不住后面跟着的命令。
如果你撞上了黑名单,工具调用会返回一条说明性错误给 Agent,命令不会执行。如果某个正当流程确实需要这些命令(比如你就是做“擦盘重装”流水线的运维),请在 Agent 之外自己跑。
自定义拒绝规则:approvals.deny
硬性黑名单是代码里写死的,approvals.deny 则是给你自己用的“可编辑版”:一串 glob 模式,匹配到的终端命令会被无条件拦截——在 --yolo、/yolo 和 approvals.mode: off 之前就先拦。适合“带例外的 YOLO”玩法:让 Agent 随便干,但有几件事永远别碰。
approvals:
deny:
- "git push --force*"
- "*curl*|*sh*"
- "dd if=* of=/dev/*"
几个要点:
- 模式用的是 fnmatch 通配符(
*、?、[...]),不区分大小写,既匹配整条命令文本,也匹配单个可执行命令候选。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 别名解析失败,{、!、:也各有各的 YAML 含义,shell 味重的内容用单引号最稳。
受监管网关的生命周期限制
终端工具还有一道独立的、不可覆盖的护栏:不允许从网关自己的受监管进程内部去停止或重启网关。自我重启可能在工具还没跑完时就把它干掉,进而引发“监管器/自动恢复”死循环。用户审批、YOLO 模式、force=True 都绕不过这道护栏。
这道护栏还会拦下那些冲着网关所用解释器镜像去的“杀进程”命令——taskkill /F /IM python.exe、taskkill /FI "IMAGENAME eq 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,再读回已加载的计划,然后才能报告“已激活”。
工具拒绝意味着命令没有通过那次工具调用执行。助手自己决定不发这个调用,是另一回事(模型决策);换模型不会改变终端护栏的策略。
控制文件别直接改
有一点得提醒你:别让 Agent 直接去 patch ~/.hermes/cron/jobs.json 这类控制文件。想改定时任务,走 cronjob_manage 工具、hermes cron 或者 /cron 命令——它们都是通过受支持的 API 去更新任务存储的。其他 Hermes 控制文件也一样,写入安全机制会挡住直接编辑。
环境变量透传:别把基础设施密钥塞进去
有些场景下,你会想让 Agent 跑的命令能读到某些环境变量,这个是通过 env_passthrough 配置的。但这里有一条硬规矩:Hermes 自己的基础设施密钥——provider 的 API key、网关 token 这些——绝对不能加进 env_passthrough。它们有专门的机制去管理,不走这条路。
具体来说:你在配置里声明这类名字时,会被直接拒绝;而如果某个名字你声明的时候还没事,后来被某个平台适配器“认领”了(比如技能加载之后,某个插件适配器才注册进来),那从那一刻起,这个名字就不再被透传了。
TLS 证书信任
Hermes 通过 truststore 来初始化平台的证书校验器:Windows 用系统证书库,macOS 用系统信任服务,Linux 用 OpenSSL 的系统信任路径。如果初始化失败,Hermes 会记一条日志,然后退回 OpenSSL 默认设置。
如果你在公司 TLS 代理后面,请按你们组织的操作系统信任流程安装代理的根证书。Hermes 的 provider 解析器不再通过 HERMES_CA_BUNDLE 或过去那套 CA 环境变量阶梯来选择信任。沙箱子进程可以有自己的独立 CA 配置。以前那个启动时的证书守卫也一并去掉了:Hermes 不再校验 HERMES_CA_BUNDLE / SSL_CERT_FILE / REQUESTS_CA_BUNDLE 这些变量。
其他几个贴心小设置
cron_mode、single_query_mode和unattended_mode:默认都是deny。意思是,当定时任务、单次查询脚本,或者 webhook/API 这类无人值守会话遇到需要审批的命令时,直接拒绝执行,让 Agent 自己想办法绕开,而不是卡在那儿等你(因为也没人等)。unattended_mode覆盖的是 webhook、msgraph_webhook、api_server 这些无人值守平台——它们跟前两者一样,没有活人能回/approve,所以deny是立刻拦下命令,而不是干等整个审批超时。有个例外:如果 api_server 会话的客户端能回审批卡片(/v1/runs和流式聊天补全,走POST /v1/runs/{id}/approval),那它照样会收到审批请求。destructive_slash_confirm:当你输入/clear、/new这类会清空当前对话历史的命令时,它会弹窗问你“确定吗?”,防止手滑误删。
总之,Hermes 的安全设计就是一套组合拳,让你在享受 AI 带来的便利时,心里能踏实不少。记住,默认的 smart 模式是最佳选择,既省心又安全。
📖 官方文档
本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/security