How to connect the Hermes Desktop App to a Remote Hermes Backend (the clean way)

How to connect the Hermes Desktop App to a Remote Hermes Backend (the clean way)

C
Cyris
Guides3 June 2026
All writings
How to connect the Hermes Desktop App to a Remote Hermes Backend (the clean way)

Running the Hermes agent on a spare always-on machine (a home server, a mini PC, or in my case a MacBook Air sitting on a shelf) and driving it from the native desktop app on your laptop is a great setup. The agent gets a stable home with your files, sessions and tools, and you get the native UI from anywhere.

Getting this working used to mean SSH tunnels, Cloudflare proxies and Host-header rewrites. Not anymore. A recent Hermes update made it clean. Here is how to connect the desktop app to a remote Hermes backend, why it was painful before, and how to expose it safely over Tailscale.

โš ๏ธ Version requirement. You need a recent Hermes build on the server (June 2026 or later). Older builds reject the desktop app's WebSocket and you will hit the reconnect loop below. Just run hermes update.

TL;DR

On the machine running Hermes, pin the session token in ~/.hermes/.env so it survives restarts:

HERMES_DASHBOARD_SESSION_TOKEN=your-long-random-token-here

Then start the dashboard:

hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build

Then in the desktop app, go to Settings > Gateway > Remote connection and enter:

  • Remote URL: http://<server-ip>:9119
  • Session Token: the value above

The rest of this post walks through it properly, in order.

Why this used to be hard

The desktop app is an Electron shell. When it connects to a remote dashboard it does two things: a REST handshake (GET /api/status), then a live WebSocket for the chat and event stream. The REST call almost always works, which is why "Test connection" looks fine. The WebSocket has three extra guards that quietly reject it:

  1. The chat WebSocket is off by default. Without --tui, the embedded-chat channels (/api/ws, /api/events) do not exist, so the upgrade returns 403. The app connects, the socket fails, and it loops forever on "reconnecting / repair".
  2. A Host-header allowlist (a DNS-rebinding defense) only accepts the host the dashboard is bound to.
  3. An Origin guard. The packaged Electron renderer loads from a file:// origin, and older Hermes builds rejected non-web origins on any non-loopback bind.

Put together, a dashboard bound to 0.0.0.0 served REST fine but refused the desktop app's WebSocket. People worked around it with loopback binds plus SSH tunnels, or reverse proxies that rewrote Host to localhost and stripped Origin.

The recent fix removed all of that. On an --insecure bind, where the session token is the auth, Hermes now accepts the desktop app's file:// origin directly. A DNS-rebinding attacker on a website cannot forge a file:// origin and hold your token at the same time, so the guard was not buying anything there. The maintainers relaxed it.

Step 1: Stop your current Hermes

If you already have a dashboard running, stop it first so the new one can bind the port cleanly:

pkill -f "hermes dashboard"

If you run Hermes under launchd or systemd, stop that service instead, otherwise it will respawn the old config. Confirm nothing is still holding the port:

lsof -nP -iTCP:9119 -sTCP:LISTEN

Step 2: Persist the session token

By default Hermes mints a new random session token every time the dashboard starts. That is fine once, but every restart or reboot silently invalidates the token saved in your desktop app, and you are back to re-pasting it.

Pin it once. Add this to ~/.hermes/.env on the server:

HERMES_DASHBOARD_SESSION_TOKEN=your-long-random-token-here

Generate a strong value with openssl rand -base64 32. Hermes reads .env at startup, so from here on the token stays the same across restarts and the desktop app keeps working untouched.

๐Ÿ’ก Where the token lives. ~/.hermes/.env is where your API keys already live, so it is the natural home for this. Keep the file readable only by your user.

Step 3: Start the dashboard with the right flags

Now start it:

hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build

What each flag does:

Flag Why it matters
--host 0.0.0.0 Binds all interfaces so the dashboard is reachable from other machines, including your Tailscale interface. A 127.0.0.1 bind is loopback only and cannot be reached remotely.
--port 9119 Default dashboard port.
--insecure Uses simple session-token auth instead of the full OAuth gate. Required for token-based remote clients like the desktop app. Use only on trusted networks (see Tailscale below).
--tui Critical. Turns on the embedded-chat WebSocket channels. Without it the app loops on 403.
--no-open Do not pop a browser on the server.
--skip-build Optional. Skips rebuilding the web UI and uses the prebuilt assets for faster startup.

Sanity check from another machine. A working WebSocket upgrade returns 101, not 403:

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Origin: file://" \
  "http://<server-ip>:9119/api/ws?token=<your-token>"

Step 4: Point the desktop app at the backend

Easiest route is the UI. Go to Settings > Gateway > Remote connection, enter the Remote URL (http://<server-ip>:9119) and the session token, and reconnect.

If you would rather skip the UI, or stop the app bootstrapping its own local Hermes install on first launch, set these before launching the app:

export HERMES_DESKTOP_REMOTE_URL="http://<server-ip>:9119"
export HERMES_DESKTOP_REMOTE_TOKEN="your-long-random-token-here"
# then launch Hermes.app / Hermes.exe

On macOS the app stores this in ~/Library/Application Support/Hermes/connection.json (equivalent app-data folder on Windows and Linux). The UI and the env vars both write there, so you rarely need to touch it by hand.

Step 5: Reach it securely over Tailscale

--insecure means anyone who can reach <server-ip>:9119 can read the token from the page and connect. Do not expose that port to the public internet. Tailscale is the clean answer. It puts both machines on a private WireGuard network, so the dashboard is reachable from your devices and nothing else.

  1. Install Tailscale on both machines and sign into the same tailnet.

  2. Find the server's tailnet IP (a 100.x.y.z address). Run tailscale ip -4 on the server, or tailscale status from your laptop.

  3. Use that IP as the Remote URL in the desktop app:

    http://100.x.y.z:9119
    

Because the dashboard is bound to 0.0.0.0, it is already listening on the Tailscale interface, so there is nothing extra to configure. The Tailscale IP (or its MagicDNS name like your-server.tailXXXX.ts.net) works as-is, because a 0.0.0.0 bind accepts any Host header.

Security notes:

  • The tailnet is your trust boundary. --insecure is fine here precisely because only your own devices can reach the port.
  • Lock it down further with Tailscale ACLs so only the devices that need the dashboard can reach port 9119.
  • Do not use Tailscale Funnel (public exposure) for the raw dashboard port. If you genuinely need public browser access, run the OAuth-gated mode behind a reverse proxy instead of --insecure.

Step 6: Make it survive reboots

You will want the dashboard to come back on its own. On macOS, a LaunchAgent at ~/Library/LaunchAgents/ai.hermes.dashboard.plist does the job (point it at your hermes binary):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>ai.hermes.dashboard</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/you/.hermes/hermes-agent/venv/bin/hermes</string>
        <string>dashboard</string>
        <string>--host</string><string>0.0.0.0</string>
        <string>--port</string><string>9119</string>
        <string>--insecure</string>
        <string>--tui</string>
        <string>--no-open</string>
        <string>--skip-build</string>
    </array>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
    <key>StandardOutPath</key><string>/Users/you/.hermes/logs/dashboard.log</string>
    <key>StandardErrorPath</key><string>/Users/you/.hermes/logs/dashboard.error.log</string>
</dict>
</plist>

Load it with launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.dashboard.plist. The token stays stable because it is pinned in ~/.hermes/.env, which the launched process reads, so no secret needs to live in the plist.

On Linux, a systemd unit does the same (or a drop-in with Environment="HERMES_DASHBOARD_TUI=1" if you start the dashboard another way).

Troubleshooting

Symptom Cause Fix
App loops on "reconnecting / repair", REST works Missing --tui, or Hermes too old Add --tui; run hermes update
WebSocket returns 403 with file:// origin Old Hermes (origin guard) Run hermes update to a June 2026 or later build
"Invalid Host header" on the socket Dashboard bound to 127.0.0.1 but reached via an IP Bind --host 0.0.0.0 (accepts any Host)
Connects, then drops after a restart Token rotated Pin HERMES_DASHBOARD_SESSION_TOKEN in ~/.hermes/.env
Auth fails out of nowhere Stale token in the app Re-copy the current token, or confirm the pinned one is set

Wrapping up

It comes down to three things: --tui to turn on the chat WebSocket, --insecure so token auth (and the desktop's file:// origin) is accepted, and a pinned HERMES_DASHBOARD_SESSION_TOKEN so the connection survives restarts. Put the server on Tailscale and you have a private, always-on agent you can drive from the native app anywhere. No tunnels, no proxies, no workarounds.

How was this article?

Share this article

If you found this helpful, consider buying me a coffee

100% of donations are compiled directly into code.