A Flutter client for Hermes Agent
(by Nous Research)'s web dashboard. Connect to a
self-hosted hermes dashboard, sign in with an OIDC provider or a
username and password, and chat with your agent from iPhone, iPad, Mac and
Android.
- Chat with history, streaming replies, tool calls, and approval and question requests from the agent
- Play voice messages from the agent inline, including OGG/Opus and MP3
- A thread list with pinning, archive and delete, plus profiles and messaging connections
- Bot Mode specialists with profile-backed identities, a canonical chat for each bot, and hosted group rooms on compatible Hermes dashboards
- A Kanban board, when the server has the Kanban plugin on
- Notifications when a reply finishes or the agent needs you
- A share extension on iOS and a share inbox on macOS, and an Apple Watch companion
- OpenTelemetry export, off unless you configure it
Hosted groups can run on Hermes dashboards with protocol 2 support. Clarify, sudo, and secret requests from group members cannot be answered in the room; if a member waits for one, use Stop. Direct bot chats support their own input requests.
Download the app for iPhone, iPad and Apple Watch from the App Store, or join the TestFlight beta for iPhone, iPad and Mac.
The app checks for a newer version when it opens and offers to take you to it: in the App Store on iOS and macOS, on Google Play on Android, and on the GitHub release page on Linux and Windows. It finds nothing until the app is listed in a store.
You need your own Hermes Agent dashboard to sign in (see Getting started). This is an independent app and is not affiliated with Nous Research.
Both ship as release downloads
(a Linux tarball and .deb, an unsigned Windows x64 zip) and run the chat, the
Kanban board, schedules, memory and the settings pages as on the Mac. They get
the phone and Mac extras only where a plugin and the code allow it.
Both columns are read from the code, not run: pubspec.yaml, the plugins'
platform lists and the platform checks in lib/. CI builds both and packages
Linux but launches neither app. The Windows column is marked "from code, not
run" all the same.
| Feature | Windows (from code, not run) | Linux | Why |
|---|---|---|---|
| Sign-in (OIDC and password) | Works | Works | The loopback and PKCE flow is plain dart:io; the page opens in the default browser through url_launcher. |
| Keeping you signed in | Works | Partly | Windows encrypts the tokens with a key in Credential Manager. Linux needs a running Secret Service (GNOME Keyring or KWallet) through libsecret; without one, saving the session is expected to fail. The durable send queue keeps its key in the same store. |
| Chat, threads, profiles, Kanban, schedules, memory, plugins | Works | Works | Shared Dart code and the server's API. |
| Attachments: drag and drop, paste | Works | Works | desktop_drop and pasteboard; photo library and camera are phone-only. |
| Attachments: file picker, Save as | Works | Partly | On Linux file_picker uses xdg-desktop-portal, which must be installed. |
| Voice messages: playback | Works | Works | media_kit; on Linux it needs libmpv2 (libmpv1 on Ubuntu 22.04), which the .deb declares. CI plays Ogg/Opus, MP3 and more through libmpv on Linux (native_voice_player_test). |
| Voice messages: recording | Works | Works | Same record stream as dictation; needs PulseAudio or PipeWire-pulse on Linux. |
| Dictation (speech to text) | Works | Works | The microphone goes through record (16 kHz PCM stream) to the server's transcription. On Linux that is parecord and pactl from pulseaudio-utils, which the .deb declares. There is no on-device recognizer. |
| Voice calls | Not available | Not available | Hands-free calls are iOS only. |
| Bot Screen viewer and takeover | Works | Works | The viewer and its keyboard input have Windows and Linux paths. |
| Notifications | Works (untested) | Works (untested) | Plain notifications while the app runs: a tap opens the chat, there are no answer buttons, and nothing arrives while the app is closed. Not yet run on a real desktop. |
| App Lock | Works (untested) | Not available | Windows Hello or a PIN through local_auth. It has no Linux backend, and the dialog says so. |
Opening hermes:// links |
Not available | Not available | The link listener only starts on iOS, macOS and Android, and neither package registers the scheme. |
| Receive shared files and text | Not available | Not available | Receiving works on iOS, Android and macOS. Drag and drop and paste still work. |
| Drag files out | Not available | Not available | Drag out is macOS only. Opening a file uses the system default app. |
| Background refresh and alerts | Not available | Not available | workmanager has no Windows support; Linux only through an experimental, unendorsed workmanager_linux the app does not use. Nothing is announced while the app is closed. |
| Widgets, quick actions, Spotlight, Live Activities, Watch | Not available | Not available | Apple and Android surfaces. |
| Quick panel, menu bar item, Dock menu, conversation windows | Not available | Not available | macOS only. |
| Keyboard navigation (back and forward) | Works | Works | The desktop shortcuts are enabled for both. |
| Update check | Works | Works | Compares with the latest full (not pre-release) GitHub release and offers its page; the app does not update itself. |
| Telemetry (off unless configured) | Works | Works | Shared Dart code. |
docs/architecture/windows-linux.md has the evidence for each row and the gaps that are cheap to close.
They are rendered from the app's own widgets in a test, against a stand-in dashboard that serves a few invented chats. docs/screenshots explains how to render them again.
Run a dashboard the app can reach:
# on the machine that will host the dashboard
hermes dashboard --host 0.0.0.0 --port 9119 --no-openEnter that machine's address (for example http://192.168.1.20:9119) on the
app's first screen. The Web Dashboard docs
cover the username and password or self-hosted OIDC provider.
To run the app from source:
flutter pub get
flutter runOn Linux, install libmpv2 (libmpv1 on Ubuntu 22.04) for voice-message
playback. The .deb declares the runtime dependency; tarball users need to
install it separately.
Ask the agent for a voice reply and tap Play on its audio attachment. A
transcript button appears when Hermes includes a matching text_to_speech
call: it reveals the text passed to that tool, collapsed by default. Clips
without that source text remain playable without a transcript.
The iOS app embeds a watchOS app (ios/HermesWatch), so Xcode needs the
watchOS platform installed (Xcode > Settings > Components) even if you only run
the phone app. flutter build ios --simulator then also needs a simulator:
pass one with -d <simulator id>.
Plain HTTP works against a dashboard on your network: Android allows cleartext traffic, iOS and macOS allow local networking, and the macOS sandbox has the client and server network entitlements the loopback sign-in callback needs.
If the dashboard is only reachable through Tailscale or WireGuard, turn the VPN on first, then enter the address the server has on the VPN.
-
Tailscale, recommended: keep the dashboard on the server's own loopback address, which is Hermes' default, and publish it to your tailnet with
tailscale serve:hermes dashboard --port 9119 --no-open tailscale serve --bg 9119
Enter the
https://<machine>.<tailnet>.ts.netaddress thattailscale serveprints. It carries a real certificate, it stays off the internet, and on iOS Tailscale's VPN On Demand can start the VPN when the app looks up a*.ts.netname. Hermes sees these requests as local, so it does not ask anyone to sign in: every device on your tailnet can open the dashboard. Limit that with Tailscale access rules, or use the next option. -
Tailscale or WireGuard address: start the dashboard with
--hostset to the server's VPN address, for examplehermes dashboard --host 100.101.102.103, and enterhttp://100.101.102.103:9119. Hermes refuses to bind to anything but loopback until a sign-in method is configured (a password inconfig.yaml, or an OAuth provider), so set that up first; the app then asks you to sign in. Do not use0.0.0.0, which also listens on your LAN. The VPN encrypts the traffic, so plain HTTP is fine.
The VPN has to be on for the whole device, because signing in opens the system browser, which must reach the dashboard too. When the address looks like a VPN or private address and the server does not answer, the app asks whether the VPN is connected. When the network changes or you return to the app, it tries the saved server again by itself.
A reverse proxy with a certificate from its own authority, such as Caddy's
tls internal, works once the device trusts that authority's root
certificate. The system browser that signs you in needs it too. Caddy keeps
the root at /data/caddy/pki/authorities/local/root.crt in its container.
- iOS: open the root certificate on the device to install it as a profile, then turn on full trust for it in Settings > General > About > Certificate Trust Settings.
- macOS: add it to the System keychain in Keychain Access and set it to Always Trust.
- Android: install it as a CA certificate in Settings (search Settings for "CA certificate"). The app reads the CA certificates you installed when it starts and when you return to it. One you remove stays trusted until the app is closed and started again.
Until then the app says that this device doesn't trust the server's
certificate. A certificate from a public authority, such as the one
tailscale serve provides or one from Let's Encrypt, needs none of this.
The app uses the native-app OAuth flow (RFC 8252) that the dashboard exposes,
for OIDC and for password sign-in alike. It opens the system browser with a
PKCE challenge and a loopback redirect, redeems the returned code for a bearer
token pair kept in the OS keychain, and refreshes the token before it expires
and again on any 401. When /api/status reports auth_required: false (the
dashboard's loopback dev mode), sign-in is skipped. The flow mirrors the one in
Hermes Desktop; the code is in lib/src/auth/.
On iPhone, iPad, and Mac, Handoff can reopen a saved chat on another device. Sign into the same Apple Account on both devices, enable Wi-Fi, Bluetooth, and Handoff, and install Hermes on each device. Open a saved chat, then select Hermes from the receiving device's Handoff suggestion. Both devices must be able to reach the dashboard. Each device signs into Hermes independently.
If the receiving device uses another dashboard, Hermes asks before connecting to the incoming address. Changing dashboards clears the current session and local queued messages. App lock still requires an unlock before continuation. Handoff transfers the dashboard address, profile, and chat ID. Drafts, attachments, queued messages, and unsaved chats remain on their source device. Hosted group rooms and browser continuation are not supported.
The optional Hermes App messenger plugin registers the app as a Hermes messaging destination. Install it on your Hermes server to let the agent and scheduled jobs send text into new, saved chats you can open and reply to. Delivery works while the app is disconnected. The app announces new messages with a notification on Android, iOS, macOS, Windows and Linux: within about a minute while it is open, and on iOS and Android when the system lets it check in the background. A closed or suspended app is not woken at once. The plugin guide covers setup and testing.
Most REST calls go through packages/hermes_api, a client generated from
openapi/hermes-agent.openapi.json. Don't edit it by hand. Regenerate it with
./scripts/generate_hermes_api_client.sh (needs a JDK and the Dart SDK); CI
fails if the client drifts from the spec. openapi/README.md
has the details.
CLAUDE.md covers the commands and an architecture map; docs/architecture/ has each area in full. The repo uses
dart_pre_commit to run the same
dart format and dart analyze checks CI does, on staged Dart files:
flutter pub get
dart run tool/setup_git_hooks.dartThe hook is installed once per clone and is shared by all its git worktrees.
OpenSpec changes live in openspec/. Its six core workflow skills are
available to Codex in .agents/skills/ and to Claude Code in .claude/skills/.
In Codex, use $openspec-propose to plan a change and
$openspec-apply-change to implement it; the desktop app also offers them in
the Skills selector. Claude Code uses /opsx:propose and /opsx:apply.
Refresh both integrations with the version used to generate them:
npm exec --yes --package @fission-ai/openspec@1.14.0 -- openspec init --tools claude,codex --profile core --no-animationEvery widget and screen is shown in each of its states, in light and dark, on phone and desktop widths, in a Widgetbook catalog. It is published at https://cedricziel.github.io/hermes-app/ and needs no server. Run it locally with:
flutter run -d macos -t widgetbook/main.dartThe project uses the oss plugin from
cedricziel/claude-plugins
(commit discipline, stacked PRs, tests) for anyone working on it with Claude
Code:
/plugin marketplace add cedricziel/claude-plugins
/plugin install oss@cedricziel
Apache License 2.0. See LICENSE.





