安全设置——放心让 AI 帮你干活
Hermes Agent 官方教程第九篇:安全。命令审批、文件保护、容器隔离。
安全设置——放心让 AI 帮你干活
咱们写代码、跑命令的时候,最怕啥?怕 AI 一个手滑,把咱辛辛苦苦写的文件给删了,或者执行了什么危险的系统命令。别担心,Hermes Agent 在安全这块儿,那是下了血本的。今天咱就用人话聊聊,它到底是怎么给你“上保险”的。
八道防线,层层设卡
Hermes 的安全模型,你可以理解成进小区要过八道门禁:
- 用户授权:不是谁都能来使唤你的 Agent,得在“白名单”里才行。
- 危险命令审批:这是最重要的一道关卡,咱们下面细说。
- 文件写入保护:给
write_file这类操作加了个“黑名单”和“沙箱”,不让它乱写乱改。 - 容器隔离:把 Agent 关在 Docker 或 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(关闭模式):所有检查全部关闭,相当于“裸奔”。强烈不建议在正式环境这么干,除非你在 CI/CD 容器里。
顺带提一句:审批提示超时之后是没法“续杯”的——那条待审批记录会被丢掉,Agent 也会被告知这一轮别自己重试。想跑的话,重新发条消息让它再来一次就行(比如“现在跑吧”),它会发起一次新的工具调用,弹出一张新的审批卡片;而“只批一次”只对那一次调用有效。超时不算拒绝,所以再问一次完全没问题。
有个“一键放飞”的功能叫 YOLO
想彻底放飞自我?可以用 YOLO 模式。它会跳过所有危险命令的审批提示。有三种开启方式:
- 启动时加参数:
hermes --yolo - 对话里输入:
/yolo(这是个开关,再输一次就关掉) - 设置环境变量:
HERMES_YOLO_MODE=1
开启后,会话顶部会有一条红色的 ⚠ YOLO mode 警告横幅,状态栏里也会一直有个 ⚠ YOLO 的小标志,时刻提醒你:“哥们儿,现在没人管你了啊!”
注意:YOLO 模式也不是啥都不管,它依然会拦截那个“硬性黑名单”里的命令。但除此之外,它真的是“神挡杀神,佛挡杀佛”了。建议只在完全可信的环境里用,比如一次性测试容器。
硬性黑名单: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,再读回已加载的计划,然后才能报告“已激活”。
工具拒绝意味着命令没有通过那次工具调用执行。助手自己决定不发这个调用,是另一回事(模型决策);换模型不会改变终端护栏的策略。
其他几个贴心小设置
cron_mode、single_query_mode和unattended_mode:默认都是deny。意思是,当定时任务、单次查询脚本,或者 webhook/API 这类无人值守会话遇到需要审批的命令时,直接拒绝执行,让 Agent 自己想办法绕开,而不是卡在那儿等你(因为也没人等)。destructive_slash_confirm:当你输入/clear、/new这类会清空当前对话历史的命令时,它会弹窗问你“确定吗?”,防止手滑误删。
总之,Hermes 的安全设计就是一套组合拳,让你在享受 AI 带来的便利时,心里能踏实不少。记住,默认的 smart 模式是最佳选择,既省心又安全。
📖 官方文档
本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/security