A single-file client for a self-hosted Hermes gateway. Installs from Safari or Chrome as a real home-screen app, talks straight to the dashboard API, and adds no state of its own — the gateway's database stays the single source of truth across CLI, Telegram, desktop and phone. Start a conversation at your desk, carry it to the sofa, finish it from another country.
Mobile-first, and it expands to a proper desktop layout above 700px.
Chat · the week ahead · a source asking permission to notify you · every profile with its model
This is not an app you expose to the world, and it is not an app that phones home. It runs over Tailscale, on your own private network, and that is a requirement rather than a suggestion.
What that buys you:
- No open ports. No domain. No certificates. No reverse proxy. Nothing on your server is reachable from the internet, so there is no login page for anyone to find, and no attack surface to harden.
- Your conversations stay between your devices and your box. Traffic is end-to-end encrypted between machines you own. No third party sees your prompts, your files, your diary or your agent's output — because none of it crosses anyone else's infrastructure.
- No account with us, because there is no us. This is a static HTML file on your own server. There is no service behind it, no telemetry, and nothing to sign up for.
The whole install is six lines and no networking knowledge (see below). If you'd rather use your own reverse proxy, you can — but then TLS, authentication and exposure are yours to get right, and getting them wrong puts an authenticated agent with shell access on the open internet. Tailscale is the recommended path for a reason.
Streaming replies with a live thinking indicator and tool-call chips, so you can see what it's doing rather than watching a spinner. Attach images, PDFs or files. Dictate with the mic — including the iOS quirk where Safari re-commits dictated text over a cleared box, which took three attempts to beat. Have answers read back to you.
Steer without interrupting. Mid-run, ⤳ injects a note into the agent's next tool result instead of stopping it — the difference between "no, the other repo" and losing five minutes of work.
A context gauge that isn't decorative. The gateway computes real occupancy, compression-aware; the client just shows it. Grey, amber at 70%, red at 88%, and tapping it gives exact token counts. It never invents a number when the engine reports none.
Tabs across the top, each with its own live WebSocket state. Background chats keep streaming while you read another; a pulsing dot means running, green means it finished while you were away. Desktop Hermes doesn't do this.
Hermes keeps sessions in a separate database per profile, so anything that only reads the default profile is blind to most of your history. This client aggregates across all of them: one recent list, tagged by profile, full-text search fanned out across every database, paged 60 at a time, and any session resumable in the profile it belongs to.
That last part is fiddlier than it sounds — resuming a session means telling the gateway which profile's home to load, or the agent comes back without its skills, model or prompt. Handled, including across the WebSocket drops that phones do constantly. A resume that genuinely fails says so and offers Retry; it never silently starts a fresh conversation somewhere else.
The 📅 tab shows the next seven days grouped under day headers — "Today — Wed 15 Jul", "Tomorrow", then dates — with a freshness stamp and a refresh button, because a diary that quietly serves you stale data is worse than one that admits it's stale.
The calendar itself is yours to plug in: drop a calendar_source.py next to the sidecar exposing one function, fetch_calendar(). A worked Google example ships in examples/. Without one the tab says so politely and everything else carries on.
The part worth stealing. Any script, cron or Hermes skill can POST /notify — but an unknown sender doesn't reach your phone. It's held pending, you get one meta-push a day ("「backup-monitor」 wants to send you alerts"), and you approve or mute it in the app, exactly like an iOS permission prompt. Approving flushes the queue; muting discards it silently.
An Alerts tab gives you a master switch and one toggle per sender. Master off drops approved senders and registers new ones quietly, so turning it back on doesn't bury you.
Beyond that: a "day ahead" push each morning, a reminder before each timed event, and a mount-aware service worker that focuses your existing window instead of opening a fourth copy.
Tap the model pill and the conversation you're in changes model — not the next one. The gateway has no model.set RPC, so this drives the same /model --session path the CLI uses, deliberately scoped to the session: a tap on your phone must never rewrite the global default in config.yaml. Reasoning effort is set per new chat and labelled as such.
Sessions browse, search and page, with a two-tap delete. Crons across every profile (read-only — the gateway's cron RPC can't scope mutations to a profile, so an editor here would be a trap). The file browser, gateway status, and tool-approval prompts with once / session / always / deny.
Screen wake lock while an answer is streaming, so iOS auto-lock stops killing the socket mid-reply.
You have options, and it's worth knowing them before you install anything.
Hermex is a native iPhone app on the App Store — free, polished, and genuinely good at what it does: chat, kanban boards, attachments, workspace browsing, LaTeX, multi-server switching, offline caching of recent sessions. If you want a native app from the App Store and you're on iOS 18 or later, use it. It's not affiliated with Nous Research, and it isn't open source.
Official mobile doesn't exist yet. There are three competing pull requests proposing React Native shells around the desktop renderer, open since June 2026, awaiting an architecture decision. It'll land eventually and it'll probably be good.
This is a different trade. Where it differs, and why you might want it:
| Hermes Mobile | Native app | |
|---|---|---|
| Can you read it? | One HTML file, ~1,900 lines, no build step, no framework, and it loads nothing from the internet | A compiled binary you point at a server where your agent has shell access |
| What's exposed? | Nothing. Tailnet-only by design | Needs your server "reachable" — usually the public internet |
| What runs it? | Any modern browser: iPhone, Android, Windows, Mac, Linux | iOS 18+, iPadOS, visionOS |
| Install | Copy six files, six lines of tailscale serve |
App Store |
| Notifications | Per-source permission gate, master switch, urgent-only routing | — |
| Diary | Week ahead from your own calendar source, morning push, event reminders | — |
| Parallel chats | Up to 8, each streaming in the background | — |
| Profiles | Every profile's sessions in one searchable list | — |
The honest summary: a native app will always feel more native. This is for people who'd rather run something they can audit in one sitting, on a network nobody else can reach, and who want the notification and diary machinery that nothing else has.
- No accounts, no backend of its own, no build step, no framework. One HTML file you can audit in a sitting.
- No offline mode — it's a thin client; the agent is the product.
- No public internet exposure, ever. Tailnet-only is the design, not a default you can casually override.
Four things the app needs. Miss one and the failure is usually silent, so they're worth reading once:
- Same origin as the gateway. Every request is
credentials: "same-origin"against the dashboard's/api/*, and the WebSocket is built fromlocation.host. The PWA and the gateway must be one origin. Any reverse proxy can arrange this; Tailscale serve is simply the shortest way. - One mount path, and not the site root if you want push. Serve the six files together under a single path. Everything derives that path at runtime, so
/m,/mobileor/phoneall work (see below). The sidecar's routes live at<mount>/api, which collides with the gateway's own/apiif the mount is/— so with push enabled, mount somewhere. - HTTPS. Service workers and Web Push require a secure context. Over plain
http://the chat works and push silently does not. Tailscale serve gives you TLS for free;http://localhostalso counts as secure for local testing. - An auth provider on the gateway (e.g.
HERMES_DASHBOARD_BASIC_AUTH_USERNAME/_PASSWORD) — the dashboard refuses to start unauthenticated off loopback anyway.
index.html and sw.js work the mount path out for themselves (from location.pathname and the service worker's registration scope), and the manifest's icons are relative. Only manifest.json's start_url/scope need the literal value — they can't be relative, because a relative one resolves with a trailing slash and would then fail to match the page:
sed -i 's#"/m"#"/mobile"#g' manifest.json # start_url + scopeThen mount the files under /mobile instead. Nothing else changes.
On the host running your Hermes gateway (dashboard on 127.0.0.1:9119):
git clone https://github.com/Talaria-D/Hermes-Mobile-App /opt/hermes-mobile
tailscale serve --bg --set-path / http://127.0.0.1:9119
tailscale serve --bg --set-path /m /opt/hermes-mobile/index.html
tailscale serve --bg --set-path /m/sw.js /opt/hermes-mobile/sw.js
tailscale serve --bg --set-path /m/manifest.json /opt/hermes-mobile/manifest.json
tailscale serve --bg --set-path /m/icon-180.png /opt/hermes-mobile/icon-180.png
tailscale serve --bg --set-path /m/icon-512.png /opt/hermes-mobile/icon-512.pngThen on your phone (with Tailscale connected): open https://<your-machine>.<tailnet>.ts.net/m, sign in with your Hermes dashboard credentials, Share → Add to Home Screen. Done — TLS, auth, and reachability are all Tailscale's problem, which is to say: solved.
The gateway needs an auth provider configured (e.g. HERMES_DASHBOARD_BASIC_AUTH_USERNAME / _PASSWORD) — an unauthenticated non-loopback dashboard refuses to start by design.
cd /opt/hermes-mobile && python3 -m venv venv && venv/bin/pip install pywebpush
cp examples/calendar_gog.py calendar_source.py # then adapt fetch_calendar() to YOUR calendar
PWA_PUSH_EMAIL=you@example.com venv/bin/python sidecar.py
tailscale serve --bg --set-path /m/api http://127.0.0.1:9210VAPID keys generate themselves on first run. Wire POST /m/api/run to a cron (a Hermes script-cron keeps it visible in Hermes) for reminders. In the app: Alerts tab → Enable push.
Point any script at POST /notify {"source":"my-script","title":"…","body":"…"} — the first-use approval gate does the rest.
Any reverse proxy works, so long as it puts the gateway and the PWA on one origin. The shape, in nginx terms:
location / { proxy_pass http://127.0.0.1:9119; } # gateway (needs WebSocket upgrade headers)
location /m { alias /opt/hermes-mobile/index.html; }
location /m/api/ { proxy_pass http://127.0.0.1:9210/; } # sidecar, if used
location /m/ { alias /opt/hermes-mobile/; } # sw.js, manifest, iconsThe / proxy needs the usual proxy_set_header Upgrade/Connection pair, or the chat WebSocket won't connect. TLS is on you here — see requirement 3.
Core — chat, sessions, profiles, crons, files — works anywhere modern. The extras are feature-detected individually and degrade quietly rather than breaking the app:
| Chrome / Edge | Safari | Firefox | |
|---|---|---|---|
| Chat, sessions, crons, files | ✅ | ✅ | ✅ |
| Install as an app | ✅ | ✅ | desktop only |
| Push notifications | ✅ | iOS 16.4+, installed to home screen only | ✅ |
| Screen wake lock while answering | ✅ | 16.4+ | ❌ (one-time note suggests Auto-Lock → Never) |
| Voice dictation | ✅ | ✅ | ❌ (button hidden) |
| Read answers aloud | ✅ | ✅ | ✅ |
iOS is the strictest target, not the intended one — Android and desktop have fewer conditions on push, not more.
This client drives the Hermes dashboard's internal API (/api/sessions, /api/profiles/sessions, /api/ws JSON-RPC, …). Internal means it can change between Hermes releases without notice. Tested against Hermes v0.18.0 and v0.19.1 ("Quicksilver").
smoke.sh is the compatibility gate: it probes every endpoint the PWA depends on, unauthenticated (live routes answer 200/401/422; removed routes answer 404/405). Run it against a candidate gateway before upgrading Hermes:
./smoke.sh http://127.0.0.1:<candidate-port>Exit 0 → safe to upgrade. Exit 1 → the contract moved; patch the PWA (or wait) first.
Worth being straight about, so bug reports land in the right place:
- Tested in anger: iOS Safari as an installed home-screen PWA, against Hermes v0.18.0 and v0.19.1, served over Tailscale, gateway in Docker. Daily driver for months.
- Should work, untested: Android, Windows, desktop browsers generally, non-Tailscale reverse proxies, mounts other than
/m, and Hermes installed natively rather than in Docker. Nothing in the client knows or cares about any of that — it's one static file speaking HTTP to the dashboard, and the platform features above are all standards with feature detection around them — but "no reason it shouldn't" is not the same as "someone ran it". Reports welcome.
- Designed for tailnet-only serving; nothing here should ever face the public internet.
- The PWA holds no secrets — auth is the gateway's session cookie, push keys live server-side with the sidecar.
- The sidecar binds loopback (plus whatever you put in
PWA_PUSH_BINDS, e.g. a docker bridge IP so in-container skills can notify) and stores push subscriptions in a local JSON file, git-ignored. - Single-file client:
index.htmlis the entire attack surface reviewable in one sitting.
MIT — see LICENSE.



