Reach your Hermes agent from your phone
Your Hermes agent runs on a VPS. Reach it from a phone over a private mesh or a public HTTPS endpoint, and revoke access when the phone goes missing.
The three ways to reach your Hermes agent from your phone
To reach your Hermes agent from your phone you use its web dashboard, and the real decision is what sits in front of that dashboard. Option one is a private mesh network, where the dashboard listens only on a Tailscale address and no port faces the internet. Option two is a public endpoint behind a reverse proxy with TLS (transport layer security) and a login, where that login is the only thing protecting a terminal into your server. Option three is SSH (secure shell) from a terminal app, which takes the least setup and hands over the most access.
This guide starts where running a Hermes agent on your own VPS finishes. The agent is installed, it answers on the command line, and you now want it in your pocket.
The commands below are written against tag v2026.8.3, published on 3 August 2026 and labelled v0.20.0 in the release notes. Hermes ships often, so pin the machine to a tag instead of following main. The installer accepts a commit:
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --commit 3c27eb6234bf
hermes versionhermes version prints what is actually installed. Compare it with the tag before you trust any flag on this page.
What the repository ships, and what it does not
There is no iOS or Android app in the Hermes repository. The native client is hermes desktop (alias hermes gui), an Electron application for macOS, Windows and Linux. It can attach to a remote agent instead of a local one: set the remote URL to http://<server-address>:9119 in Settings under Gateway, or export HERMES_DESKTOP_REMOTE_URL=http://<server-address>:9119 before launching it. That solves a second laptop. It does nothing for a phone.
The surface you use from a phone is hermes dashboard, the browser interface. It manages configuration, sessions, cron jobs, skills, logs and messaging platforms, and it embeds a terminal: the server spawns hermes --tui behind a PTY (pseudo-terminal) and streams the output into the page. On a narrow screen the session switcher folds into a slide-over panel, so the layout survives a phone.
By default the dashboard listens on 127.0.0.1:9119 and opens a browser tab on the server itself, which is no use on a headless VPS (virtual private server). Start it without the browser and look at the socket:
hermes dashboard --no-open
ss -lntp | grep 9119A listener on 127.0.0.1:9119 is reachable only from the server. A listener on 0.0.0.0:9119 is reachable from every network the machine is attached to, including the public one. That one line decides how exposed you are, so read it instead of assuming it.
Option 1: a private mesh with Tailscale
Pick this one unless you have a reason not to. Tailscale is a mesh VPN (virtual private network) built on WireGuard. Every device you enrol gets a stable address and talks directly to the others. Your phone joins the same mesh, so the dashboard never needs a public port at all.
On the server:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale ip -4Install the Tailscale app on the phone and sign in with the same account. tailscale status on the server then lists the phone by name. Now bind the dashboard to the address that tailscale ip -4 printed:
hermes dashboard --host 100.101.102.103 --port 9119 --no-openBinding to the mesh address, rather than to 0.0.0.0, means the listening socket exists only on the Tailscale interface. A port scan of your public IP address finds nothing on 9119 because nothing is listening there, so your firewall is no longer the only thing saving you. Confirm it with ss -lntp | grep 9119 again.
The bind address also decides authentication, and this is the part people miss. On 127.0.0.1 the dashboard has no login. On any other address the auth gate is mandatory, and with no provider configured the server refuses to start. The project calls this failing closed. Run it interactively the first time and it offers to set up a username and password there and then. The result is written to ~/.hermes/.env:
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=you
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH=scrypt$16384$8$1$...
HERMES_DASHBOARD_BASIC_AUTH_SECRET=a-long-random-stringGenerate that secret with openssl rand -hex 32 and then leave it alone. It signs the session cookie, so changing it signs every device out. The same file holds your model API keys, so keep it private with chmod 600 ~/.hermes/.env.
Check the gate from another machine on the mesh before you trust it:
curl -s http://100.101.102.103:9119/api/status | jq '.auth_required, .auth_providers'true followed by ["basic"] means the gate is on and the password provider is the one answering. The project's own documentation is blunt about that provider: it is meant for a trusted network or a VPN, and it is not suitable for a dashboard facing the public internet. On a mesh, that is exactly the situation you are in.
For HTTPS inside the tailnet, hand the port to Tailscale:
sudo tailscale serve --bg localhost:9119
sudo tailscale serve statusTailscale terminates TLS with a certificate issued for the machine's MagicDNS name, so the browser stops warning and the session cookie gets its Secure flag. Enable MagicDNS and HTTPS Certificates on the DNS page of the Tailscale admin console first, and read the acknowledgement while you are there: your machine names and your tailnet DNS name are published to the public certificate transparency ledger, which anyone can read.
The honest cost here is the coordination server. Traffic between your phone and your VPS is encrypted end to end and does not pass through Tailscale, but the account that decides which devices belong to the mesh does not live on your hardware. If that matters, run the control plane yourself with Headscale, the self-hosted Tailscale control server, and point clients at it using tailscale up --login-server https://headscale.example.com. If you would rather drop the mesh and configure peers by hand, the comparison between plain WireGuard and Tailscale covers what you give up.
Option 2: a public HTTPS endpoint, and the trap inside it
Sometimes a mesh is not available. A work phone may not let you install a VPN client. Then the dashboard goes behind a reverse proxy. Keep the dashboard on loopback and let Caddy hold the certificate:
hermes.example.com {
reverse_proxy 127.0.0.1:9119
}Caddy fetches and renews the certificate on its own, and its reverse_proxy passes WebSocket upgrades through with no extra directives, which the dashboard's embedded terminal needs. On nginx you have to add the Upgrade and Connection headers yourself, and issuing a Let's Encrypt certificate with Certbot on nginx covers the TLS half of that job. Tell Hermes its public name so login callbacks point at the right host:
dashboard:
public_url: "https://hermes.example.com"Now the trap. The auth gate keys off the bind address, not off the address the request came from. A dashboard bound to 127.0.0.1 behind a proxy sees loopback, decides no login is needed, and serves whoever the proxy lets through. Check what you actually published:
curl -s https://hermes.example.com/api/status | jq '.auth_required'If that prints false, the internet is one URL away from a terminal on your server. No exploit is involved and nothing in the logs will complain. There are two fixes and you want at least one of them. Either put the authentication in the proxy, with Authentik as a self-hosted single sign-on provider in front of it via forward auth, or configure the dashboard's own OIDC (OpenID Connect) provider:
HERMES_DASHBOARD_OIDC_ISSUER=https://auth.example.com/application/o/hermes/
HERMES_DASHBOARD_OIDC_CLIENT_ID=hermes-dashboard
HERMES_DASHBOARD_OIDC_SCOPES="openid profile email"Even done correctly, this option has a floor you cannot lower. A public login page is a page anyone can reach and attack, and behind it is a shell on your VPS. A username and password is thin protection for that, which is why the docs push OAuth or OIDC for public binds. Rate limit the login, keep ufw closed to everything except the proxy ports and SSH, and read the access log now and then.
Option 3: SSH and mosh from a mobile terminal
The lowest-effort route: install a terminal app on the phone, load an SSH key into it, and use the agent's own text interface.
ssh you@vps.example.com
tmux new -A -s phone
hermes --tuitmux new -A -s phone attaches to a session called phone, or creates it if there is none, so the agent keeps running when the connection drops and you land back in the same place next time. The pattern is the same one used for driving Claude Code from a phone.
Plain SSH over a mobile network is uncomfortable, because the connection dies whenever the phone changes address or goes to sleep. Mosh fixes that. It runs over UDP (user datagram protocol) and syncs terminal state rather than a byte stream, so a session survives a walk from wifi to cellular:
sudo apt install -y mosh
sudo ufw allow 60000:61000/udpFrom the phone, run mosh you@vps.example.com. Mosh starts mosh-server over your existing SSH login and then moves the session to UDP in the 60000 to 61000 range, which is why that range has to be open.
The security cost is the plain one. An SSH session is a shell, and a shell is everything that account can do. The agent's approval prompts are then the last thing standing between a stolen unlocked phone and your files. Use a dedicated non-root account for this, key only, and work through hardening SSH on a VPS before you put a key on a device you carry around.
What breaks on a mobile network
Four things, and none of them are bugs you can configure away.
The dashboard logs you out while you are not looking. Access tokens have a 15 minute TTL (time to live) and the current API contract has no refresh token. When the token expires, the page sees the 401 response and navigates back to /login. Open the dashboard, put the phone in your pocket for twenty minutes, and you come back to the login screen with whatever you were typing gone. Nothing is broken. Sign in again.
The embedded terminal does not survive a background tab. The server spawns the TUI (text user interface) behind a PTY and reaps it cleanly when the tab closes, and reopening spawns a fresh one. Mobile browsers discard background tabs to reclaim memory, so switching to another app for long enough ends that terminal. Your chat history is safe, because sessions are stored on the server and hermes sessions list still shows them. The terminal itself is not something you can reattach to. If you need a session that survives, use tmux over SSH.
Idle connections die quietly. Mobile carriers drop idle TCP connections to reclaim NAT (network address translation) table entries, and the phone stops servicing the network almost entirely once the screen is off. A WebSocket that has been silent for a while is usually already dead when you unlock, and the page reconnects only after you touch it. This is why mosh feels better than SSH on a phone: it never had a connection to lose.
Nothing pushes a notification. Neither the dashboard nor an SSH session can wake your phone. There is no Hermes app on the device, so there is no path to the platform's push service. A long agent run finishes, and you find out the next time you look.
Notifications arrive through the messaging gateway
The fix for that last one is the gateway, which is a different way of reaching the agent altogether. hermes gateway connects the agent to messaging platforms including Telegram, Discord, Slack, WhatsApp, Signal and email. You then talk to your agent inside an app that already owns a push channel on your phone, already handles background delivery, and already copes with the network dropping.
hermes gateway setup
hermes gateway install
hermes gateway statushermes gateway install registers the gateway as a systemd service, so it returns after a reboot. The Hermes installer does not create any service units for you, so hermes dashboard will not come back on its own after a restart. Write a unit for whichever pieces you depend on, and writing systemd services and timers on a VPS covers the file itself.
A scheduled job can push a result the same way, with no conversation involved:
hermes send -t telegram -s "Backup report" -f /home/you/report.txtAuthorisation here is deny by default. With no allowlist set and GATEWAY_ALLOW_ALL_USERS unset, every user is refused, which is the right starting point. Name the accounts you want in ~/.hermes/.env:
TELEGRAM_ALLOWED_USERS=123456789
GATEWAY_ALLOWED_USERS=123456789Or turn on code-based pairing instead of pasting numeric IDs, by setting unauthorized_dm_behavior: pair in ~/.hermes/config.yaml. An unknown account that messages the agent receives an eight character code, and nothing else happens until you approve it:
hermes pairing list
hermes pairing approve telegram ABC12DEF
hermes pairing revoke telegram 123456789Codes expire after an hour, a user can request one every ten minutes, and five failed attempts lock that account out.
Limiting the blast radius of a stolen phone
A phone that can reach your agent is a phone that can run commands on your server, and phones get lost. Decide now what the person holding it is able to do.
Keep approvals on. approvals.mode defaults to smart, which asks an auxiliary model to judge risk and auto-approves only low risk commands. Setting it to off is the same as running everything with --yolo, and HERMES_YOLO_MODE=1 does it through the environment. Do not carry a phone that talks to an agent in that state. Set approvals.cron_mode: deny so scheduled jobs cannot approve themselves while you sleep.
approvals:
mode: smart
cron_mode: denyGive the agent a smaller world to break. terminal.backend: docker runs commands inside a container that drops all Linux capabilities except a short list, sets no-new-privileges, and caps process count, so the container becomes the boundary instead of a prompt. terminal.cwd pins the working directory. HERMES_WRITE_SAFE_ROOT restricts write_file and patch to the directories you name, and a write outside them is blocked with no approval available to override it.
export HERMES_WRITE_SAFE_ROOT=/home/you/projects:/home/you/.hermesHermes already blocks writes to ~/.ssh, ~/.aws, ~/.kube and /etc/sudoers, refuses to write .env files anywhere on disk, and rejects a hardline list of destructive commands whatever your approval settings say. Treat that as a floor, not as your plan. Run the agent as an ordinary user, never as root. The same reasoning applies to any coding agent left running unattended, and running Claude Code safely on a VPS works through it in more detail.
Then write down the revocation steps before you need them. Removing the device in the Tailscale admin console cuts mesh access at once. Changing HERMES_DASHBOARD_BASIC_AUTH_SECRET and restarting invalidates every dashboard session on every device, because that value signs the session cookie. Deleting the phone's public key from ~/.ssh/authorized_keys ends SSH. hermes pairing revoke telegram <user-id> removes a messaging account. If the device held anything that could read your provider keys, drop them with hermes auth logout <provider> and issue new ones upstream.
Check the result rather than assuming it. These are commands to run on your own box, and what they print depends on your setup:
tailscale status
hermes pairing list
hermes logs gateway -n 100
hermes status --deepThe logs live under ~/.hermes/logs/. Read them after an incident, and read them occasionally when there has not been one.
FAQ
Is there an official Hermes mobile app for iOS or Android?
No. The Hermes repository ships a command line interface, a web dashboard, and an Electron desktop client for macOS, Windows and Linux. There is no first-party iOS or Android build. From a phone you use the web dashboard in a browser, an SSH client, or a messaging platform connected through hermes gateway. Third-party mobile clients do exist, and they are third-party code holding credentials to your agent, so read the source before you install one.
Why does the Hermes dashboard log me out on my phone?
Access tokens have a 15 minute TTL and the current API contract has no refresh token. When the token expires the page receives a 401 and navigates to /login. Backgrounding a browser on a phone makes this obvious, because twenty minutes in another app is enough to hit it. Set HERMES_DASHBOARD_BASIC_AUTH_SECRET to a stable random value so the session signing key stays the same across restarts, which removes the second reason for being logged out.
Can I put the Hermes dashboard on the public internet safely?
Only with authentication you have verified yourself. The auth gate switches on based on the bind address, so a dashboard bound to 127.0.0.1 behind a reverse proxy never asks the internet for a login. Run curl -s https://your-host/api/status | jq '.auth_required' and read what comes back. Use OAuth or OIDC rather than a username and password, because the project's docs say the password provider is for trusted networks and VPNs. A private mesh avoids the question entirely, which is why it is the better default.
Do I need a Tailscale account, or can I self-host the control plane?
You can self-host it. Headscale is an open implementation of the Tailscale coordination server, and official Tailscale clients connect to it with tailscale up --login-server https://headscale.example.com. You then own the list of devices allowed onto the mesh. The cost is that you now run and back up that server, and while it is down you cannot enrol or re-authenticate a device.
How do I get a notification on my phone when the agent finishes a job?
Use the messaging gateway. The dashboard and SSH have no way to wake a phone, because there is no Hermes app on the device to receive a push. Connect the agent to Telegram, Signal, Discord or another supported platform with hermes gateway setup, then let that platform's own app deliver. A cron job can also push a single message with hermes send -t telegram -s "Job done" -f /path/to/report.txt, which delivers without calling the model.