
当 Hermes Agent 跑在一台“永远在线”的设备上,笔记本再用本地桌面端连过去,是个很常见的理想状态:Agent 有了稳定的文件、会话和工具宿主,你则保留原生桌面 UI。但这套方案曾长期被“远程 RPC 怎么调”和“WebSocket 握手容易失败”卡住。Sudolabs 有一篇很清晰的文章讲清了现状;我最近在本地实际重建过一次这条链路,其中还踩到了 hermes dashboard 前端产物缺失的问题,顺便一并写出来。
这篇文章在讲什么
把 hermes dashboard 跑在远程机器上,通过桌面端连过去,理论上一行命令就行:
1 | hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build |
但真要做“可落地”,就比想象中脏很多。核心要处理两件事:
- Dashboard 怎么安全暴露(尤其是
--insecure和 Tailscale 的配合); - 连接怎样在重启后保持不变(session token 被随机刷新是主要坑)。
下面把 Sudolabs 的方案、我实际遇到的构建错误,以及我现在的最终命令串成一个完整链路。
为什么这件事以前很麻烦
桌面端本质是一个 Electron 外壳。它连远程 Backend 时做两件事:一次 REST 握手 GET /api/status,然后建一个持续 WebSocket 推送聊天和事件流。
前几次做远程连接最容易忽略的是 WebSocket 握手,而不只是 REST。REST 往往能通关,所以“Test connection”看起来没问题,但 WebSocket 有三个额外检查会静默拒绝:
- 聊天通道默认是开的。
--tui没带时,/api/ws、/api/events不存在,协议升级会直接403,于是桌面端无限循环出现 “reconnecting / repair”。 - Host-header 白名单。这本身是一个 DNS-rebinding 防守,只接受 Dashboard 绑定主机名的 Host。
- Origin 检查。旧的 Hermes 版本在非回环绑定时,会拒绝 Electron 里的
file://origin。
这三条叠加起来,直接导致 0.0.0.0 对外绑定时 REST 能通、WebSocket 被拒。过去大家只好用 127.0.0.1 + SSH 隧道,或用反向代理改写 Host 并剥掉 Origin。
最新的修正(Sudolabs 提到的 June 2026 更新)已经把这些限制放宽了。在 --insecure 下,由于 token 本身承担认证职责,Dashboard 会直接接受 file:// origin。从攻击模型看,一个恶意网页是无法同时伪造 file:// origin 又拿到你的 token 的,所以这个 Guard 在 --insecure 场景下其实是多余的。
Step 1:停掉旧的 Hermes Dashboard
先清理可能占用 9119 的旧进程,否则 hermes dashboard 绑定会失败:
1 | pkill -f "hermes dashboard" |
如果你是用 launchd 或 systemd 管理的,要停掉对应 service。
Step 2:修复 Hermes 前端产物缺失(容易漏的坑)
在 macOS 本地这步非常容易踩坑。--skip-build 不会帮你做构建;一旦 hermes_cli/web_dist 目录不存在就会报错。
1 | ls /Users/mac/.hermes/hermes-agent/hermes_cli/web_dist |
一次完整成功输出大概是这样:
1 | > web@0.0.0 build |
如果前面已经顺手执行过 hermes dashboard --skip-build 但还没构建,一定要补这一个 step,不然桌面端会反复连不上。
Step 3:固定 Session Token,确保重启不断连
Hermes Dashboard 默认每次启动都会换随机 token。好处是安全,坏处是桌面端保存的 token 会在你重启服务后失效,又要重新粘贴。
把 token 固定到 ~/.hermes/.env:
1 | openssl rand -base64 32 |
按示例:
1 | cat << 'EOF' >> ~/.hermes/.env |
修复了这个,以后无论 hermes dashboard 还是系统重启,桌面端的连接都保持有效。
Step 4:启动 Dashboard,解释每一个 flag
1 | hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build |
| Flag | 作用 |
|---|---|
--host 0.0.0.0 |
允许从其他机器和 tailscale 接口访问;127.0.0.1 只能回环 |
--port 9119 |
默认端口 |
--insecure |
开启 token 认证,放弃 OAuth;只在可信网络里用 |
--tui |
最容易被漏掉:开启聊天 WebSocket 通道 |
--no-open |
不在远程服务器上弹浏览器 |
--skip-build |
跳过前端构建直接加载现有 web_dist |
远程机器上的 WebSocket 升级是否正常,可以用 curl 先验证:
1 | curl -s -o /dev/null -w "%{http_code}\n" \ |
返回 101 才算握手成功,返回 403 说明 --tui 漏加或 Hermes 版本太旧。
Step 5:桌面端连过去
桌面端 GUI 路径:Settings > Gateway > Remote connection,填入:
- Remote URL:
http://<server-ip>:9119 - Session Token: 上面固定的 token
如果希望跳过 GUI 预配置,也可以在启动 App 前设置环境变量:
1 | export HERMES_DESKTOP_REMOTE_URL="http://<server-ip>:9119" |
macOS 上会存储到 ~/Library/Application Support/Hermes/connection.json;Windows/Linux 也有对应路径。GUI 和 env var 是相通的。
Step 6:用 Tailscale 把服务放回“私有网”
--insecure 不是不能开,而是不能像公网那样开。Tailscale 刚好解决这个问题。
1 | # 服务端和客户端都加入同一个 tailnet |
桌面端的 Remote URL 直接改成:
1 | http://100.x.y.z:9119 |
因为 Dashboard 绑定在 0.0.0.0,Tailscale 虚拟网卡已经天然监听。只要两边同属一个 tailnet,不需要额外 NAT/代理。
附加安全建议:
- 用 Tailscale ACLs 限制只有指定设备能碰
9119; - 不要用 Tailscale Funnel 把仪表盘裸暴露到公网;
- 可视化公网访问请走 OAuth 模式 + 反向代理,而不是
--insecure。
Step 7:开机自启
macOS 上可以用 LaunchAgent,文件放 ~/Library/LaunchAgents/ai.hermes.dashboard.plist:
1 |
|
加载方式:
1 | launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.dashboard.plist |
Linux 下换成 systemd 同理。
我实际跑通的流程简版
以我当前的 macOS 环境为例,完整链路如下:
1 | # 1. 旧进程清理 |
其中,前两步是 Sudolabs 原文的基础;但我在起草这篇时,顺手也把“构建产物缺失”这段补了,因为很多人在第一篇 Follow 到这个点时,刚好会在 --skip-build 上卡一下。
Troubleshooting
| 现象 | 原因 | 修复 |
|---|---|---|
| 桌面端反复 “reconnecting / repair”,REST 正常 | --tui 漏加,或版本太旧 |
加上 --tui,并运行 hermes update |
WebSocket 上行握手返回 403 |
Origin 限制(旧版本) | 升级到 June 2026 或更新版本 |
| “Invalid Host header” | 实际是通过 IP 访问但绑了 127.0.0.1 |
改为 --host 0.0.0.0 |
| 重启后连接断开 | token 被随机重置 | 把 token 钉到 ~/.hermes/.env |
| 认证总是失败 | 桌面端缓存了旧 token | 重新粘贴或确认 .env 中的值 |
总结
这条链路靠三件事串起来:
--tui把聊天 WebSocket 打开;--insecure让远程 Dashboard 接受 token 认证和file://origin;- 固定的
HERMES_DASHBOARD_SESSION_TOKEN避免重启断开。
把 Dashboard 跑在 Tailscale 后面,就得到了一套“私有、免公网暴露、可跨设备操作”的远程 Agent 使用方式。不需要 SSH 隧道,不需要反代,也不需要 Host 改写。Sudolabs 的方案足够 Clean;真正落地时,唯一容易绊人的反倒是本地构建顺序和 token 持久化。