Nico Delgado_ Pick your service →

Field guide — Grok Bot · Hermes · Tailscale · peer dm

Grok Bot + Hermes: talk to a Mac or VPS over Tailscale (and back).

Hermes lives on my Mac and my VPS. The Grok Bot computer does not, and nothing SSHes into it. I join the box to the tailnet, bind each api_server to that machine's Tailscale IP, then the bot SSHes out and speaks with hermes peer dm.

~18 min setup CLI only peer dm both ways
A Grok Bot computer SSHes out over Tailscale to Hermes on a Mac and a VPS; the two Hermes hosts talk to each other with peer dm. Nothing SSHes into the bot. GROK BOT no inbound SSH TAILNET SSH out MAC Hermes :8642 VPS Hermes :8642
bot SSHes out → host runs hermes peer dm → Mac ↔ VPS the same way → nobody SSHes into the bot

Why bother

A cloud box that cannot see Hermes, and a Hermes that cannot SSH home

Grok Bot assistants share one cloud computer (each has its own screen). Hermes does not run on that box. The agents I actually talk to live on my Mac and my VPS. Those two do not share a filesystem with the bot, and the bot has no inbound SSH — port 22 is closed. The only direction that works is out.

Tailscale is the wire. Hermes peer dm is the conversation. I already wrote how to join the bot to the tailnet and exit through home — do that first:

Placeholders, not my networkEvery address and name below is a stand-in: YOUR_TS_IP, PEER_NAME, MAC_HOST, VPS_USER, VPS_HOST. Swap in yours. I do not print keys, tokens, or real Tailscale IPs on this page.

The steps

Bind, register, talk — both ways

Eight steps, roughly eighteen minutes if the bot is already on the tailnet and Hermes is already installed on the Mac and the VPS. Do the bind + peer add once per Hermes host.

What you need#

  • The Grok Bot computer already on your tailnet. If it isn't, stop and do the exit-node guide first.
  • Hermes on the Mac and/or the VPS — binary usually at ~/.local/bin/hermes, which is often missing from a default SSH PATH.
  • An SSH key the box can use, plus a ProxyCommand through ts nc (next step). Regular kernel TCP to 100.x will fail on a userspace Tailscale box.
  • The Tailscale IP of each Hermes host — write them down as YOUR_TS_IP. You will bind to those, not to 0.0.0.0.

The box does not run HermesI never install Hermes on the Grok Bot computer. The bot is a speaker, not a peer. Installing it there just gives you a third brain that nothing else can SSH into.

Who talks to whom#

Three roles. Only two of them run Hermes. Only two of them accept SSH.

RoleWhat it does
Grok Bot computerUserspace Tailscale. No inbound SSH. No Hermes. It SSHes out to the hosts, then runs hermes peer dm there.
MacRuns Hermes. api_server listens on this machine's Tailscale IP, port 8642. Talks to the VPS with hermes peer dm PEER_NAME.
VPSRuns Hermes, often under a non-root user. Root's home is the wrong tree — a stale ~/.hermes that looks installed and is not the one you think.

Vice versa is the same command on the other host. The thing that never works: SSH into the Grok Bot computer. Don't try Tailscale SSH at it either.

Userspace Tailscale and SSH via ts nc#

On the Grok Bot computer, Tailscale is userspace. Kernel TCP to a 100.x address fails. ts ping succeeding is not the same as TCP working. I always SSH through ts nc:

grok bot — ~/.ssh/config
Host MAC_HOST
  HostName YOUR_TS_IP
  User <mac-user>
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
  ProxyCommand ts nc %h %p

Host VPS_HOST
  HostName YOUR_TS_IP
  User root
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
  ProxyCommand ts nc %h %p

Then ssh MAC_HOST and ssh VPS_HOST just work. Point ProxyCommand at the full path to ts if it isn't on the login PATH.

Four things I keep hitting

  • Mac App Store Tailscale cannot do Tailscale SSH or tailscale file cp. Don't run tailscale set --ssh on that build. Regular sshd + a key is the path.
  • After a Grok Bot Update, Tailscale is often down. Bring it back up the same way you did in the prerequisite. Never Reset the computer — that drops Tailscale and the newer chats with it.
  • Do not set a global ALL_PROXY on the box. It breaks Cursor internals. Userspace networking is ts nc, not an HTTP proxy.
  • echo >/dev/tcp/YOUR_TS_IP/22 failing while ts ping works is the expected userspace picture, not a broken tailnet.

Bind api_server to that machine's Tailscale IP#

On each Hermes host, api_server listens on that machine's Tailscale IP only, port 8642. Not 0.0.0.0. A public bind plus a local terminal is a host-user RCE surface. Names only — generate the key on the host, chmod 600 the env file, never print the value:

mac or vps — ~/.hermes/.env (names only)
API_SERVER_ENABLED=true
API_SERVER_HOST=YOUR_TS_IP
API_SERVER_PORT=8642
API_SERVER_KEY=<generate-on-this-host-do-not-print>

Restart the gateway from SSH (step 8) so it picks up the bind. Then confirm it is listening on the Tailscale IP, not on every interface:

mac / vps — confirm the bind
# Mac
$ lsof -nP -iTCP:8642 -sTCP:LISTEN
… YOUR_TS_IP:8642 …
# VPS
$ ss -lntp | grep 8642
… YOUR_TS_IP:8642 …

One key per listenerEach host generates its own API_SERVER_KEY. The Mac's key is what the VPS will send as a bearer, and the other way around. I never paste a key into chat, a ticket, or a screenshot.

Cross-register peers#

On the Mac, add the VPS. On the VPS, add the Mac. PEER_NAME is the name you will type later. The URL is the other host's Tailscale IP. The key is the other host's listener key — pass it on the command line, don't echo it first:

each hermes host
$ hermes peer add PEER_NAME --url http://YOUR_TS_IP:8642 --key <PEER_API_SERVER_KEY>
$ hermes peer list
PEER_NAME   http://YOUR_TS_IP:8642   [key set]

Peer config stores url and note only. The keys stay in each host's Hermes .env. If peer list does not say [key set], the add did not take — do not debug by printing the env file.

Health check (uses the stored peer key, no model, no conversation):

from either hermes host
$ curl -sS -H "Authorization: Bearer $HERMES_PEER_PEER_NAME_KEY" \
  http://YOUR_TS_IP:8642/v1/models
… 200 … hermes-agent …

Load the env, don't cat itAPI_SERVER_KEY is this host's listener. Peer keys are HERMES_PEER_<NAME>_KEY. Hitting the other host with $API_SERVER_KEY 401s — use $HERMES_PEER_PEER_NAME_KEY, or curl on that host with its own $API_SERVER_KEY. Source the env. I do not cat it, and I do not put the raw key in a log.

Talk from Grok Bot#

From the Grok Bot computer I SSH to the host, then I run hermes peer dm there. SSH PATH almost never includes ~/.local/bin — export it or the command is "not found":

grok bot — talk to a peer via the Mac
$ ssh MAC_HOST 'export PATH="$HOME/.local/bin:$PATH"; \
  hermes peer dm PEER_NAME "Reply with PONG only."'

On the VPS, Hermes is often a different user than the one SSH lands on. Root's ~/.hermes is the stale tree. Run it as VPS_USER:

grok bot — talk to a peer via the VPS
$ ssh VPS_HOST 'sudo -u VPS_USER -H \
  env PATH=/home/VPS_USER/.local/bin:/usr/bin \
  hermes peer dm PEER_NAME "Reply with PONG only."'

One-shot CLI — a new session, not the live gateway brain — is hermes -z "…" the same way. It spends model quota. So does peer dm.

hermes send is not a conversationhermes send is an outbound notify. No LLM, no reply, no peer. I use peer dm when I want two agents to talk, and I do not fire -z in a loop — every call spends quota.

The other way: Mac ↔ VPS#

Once both peers are registered, the two Hermes hosts talk without the bot in the middle. Same command, other direction. Still no SSH into the Grok Bot computer — if I need a file on the box, I pull it from the Mac side with whatever connector I already use, not inbound SSH.

on the Mac / on the VPS
$ export PATH="$HOME/.local/bin:$PATH"
$ hermes peer dm PEER_NAME "Reply with PONG only."

On the VPS, prefix with sudo -u VPS_USER -H and the PATH to that user's ~/.local/bin if you are not already that user.

Done when each side answers. The bot still cannot be SSHed into — that is the design.

Restarts, pitfalls, cheat sheet#

Restart gateways from SSH. Never from inside a Hermes turn — the gateway would kill itself mid-reply.

ssh, then restart
# Mac (or as VPS_USER on the VPS)
$ export PATH="$HOME/.local/bin:$PATH"
$ hermes gateway restart
$ hermes gateway status
# VPS as root, targeting the Hermes user
$ sudo -u VPS_USER -H env XDG_RUNTIME_DIR=/run/user/<uid> \
  systemctl --user restart hermes-gateway.service

Caveats

CaveatWhat to remember
No inbound SSH on the boxPort 22 is closed. Tailscale SSH into the Grok Bot computer is not a workaround. Talk by SSHing out, then peer dm.
Userspace ≠ kernel TCPts ping can succeed while ssh YOUR_TS_IP hangs. Always ts nc / ProxyCommand.
Mac App Store TailscaleCannot do Tailscale SSH or file cp. Use regular sshd and a key.
After Grok Bot UpdateRe-bring Tailscale up. Never Reset the computer.
No global ALL_PROXYIt breaks Cursor internals on the box. Userspace is ts nc, not an HTTP proxy.
Bind stays on 100.xAPI_SERVER_HOST=YOUR_TS_IP, never 0.0.0.0. Confirm with lsof / ss.
SSH PATH~/.local/bin is missing on a default SSH login. Export it or call the binary by path.
VPS userHermes is often a non-root user. Root home is the wrong, stale tree.
Keys stay in .envPeer config is url + note. Never print keys, never paste them into a chat.
send vs peer dmhermes send is outbound notify. peer dm and -z spend model quota.
Restart from SSHNever restart the gateway from inside a Hermes turn.

Cheat sheet

all of it
$ ssh MAC_HOST
$ ssh VPS_HOST
# on each Hermes host — bind YOUR_TS_IP:8642, not 0.0.0.0
$ hermes peer add PEER_NAME --url http://YOUR_TS_IP:8642 --key <PEER_API_SERVER_KEY>
$ hermes peer list
$ ssh MAC_HOST 'export PATH="$HOME/.local/bin:$PATH"; \
    hermes peer dm PEER_NAME "…"'
$ ssh VPS_HOST 'sudo -u VPS_USER -H \
    env PATH=/home/VPS_USER/.local/bin:/usr/bin \
    hermes peer dm PEER_NAME "…"'

Who wrote this

I build, grow and automate products.

I'm a one-person product studio: custom apps, growth for apps that already shipped, and business automation with AI. Same person designs it, writes it and ships it. Pick the service that fits — an assistant will walk you through it in a few minutes and I'll read every word.

Pick your service →