💜 Enjoying Hermes for Android? ☕ Support development on Ko-fi → — donations fund new features and keep the project alive. (GitHub Sponsors coming soon.)
A native Android client for the Hermes agent gateway — a phone-friendly companion to the Hermes Desktop app. It connects to a remote Hermes gateway over Tailscale, another private VPN, or your local network, and gives you full chat plus the management surface: sessions, models, profiles, scheduled jobs, usage analytics, messaging integrations, and settings.
Built with Kotlin and Jetpack Compose (Material 3).
No build tools or developer setup needed — just grab the signed APK from the latest release and install it on your phone:
- On your Android phone, open the release link above and tap the
.apkfile to download it. - When prompted, allow installs from your browser or Files app ("Install unknown apps").
- Open the downloaded
.apkand tap Install. - Launch Hermes and complete the one-time setup — gateway URL plus either a session token (loopback) or a username/password (password-protected dashboard).
The APK is signed with the project's release key. Because it's installed outside the Play Store, Android may warn about an app from an unknown developer — that's expected.
- Chat — streaming responses, model picker, slash commands (
/…) with an inline command palette,@file mentions/path completion, image attachments, tool-call approval and clarification prompts. - Sessions — grouped by workspace, with search (instant title filter + gateway message-content search), pinning, archive / unarchive / delete (an Archived view from the Sessions top bar), and a session-admin view. Opening a session lands on the newest reply.
- Models & profiles — switch the active model or tenant profile on the fly; the active profile is shown in the chat top bar.
- Cron — list, create, edit, pause/resume, run-now, and delete scheduled jobs, with next-run times and run history.
- Usage — daily token chart and per-model breakdown.
- Messaging — enable/disable platform integrations with a guided setup flow for the required credentials.
- Settings — Server & token (edit the gateway URL and token, or username/password for a password-protected dashboard, then reconnect), appearance (System/Light/Dark + tool-call verbosity), memory, MCP servers, API keys/environment variables, and Diagnostics (a toggleable, token-redacted, shareable debug log). Configuration edits are written to the live gateway config.
- Reliability — automatic reconnect with offline banner and manual retry; an expired session re-authenticates automatically (password mode) or routes back to Setup (token mode).
| Layer | Notes |
|---|---|
| UI | Jetpack Compose + Material 3, ModalNavigationDrawer wrapping a Navigation-Compose NavHost |
| State | MVVM — ViewModel + StateFlow, collectAsStateWithLifecycle |
| DI | Hilt |
| Networking | OkHttp — REST + a WebSocket JSON-RPC stream to the gateway. Auth is either the loopback session token, or (gated dashboard) a cookie-jar session with 401 re-login and per-socket WS tickets |
| Local storage | EncryptedSharedPreferences (credentials: URL, token, username/password); DataStore (theme, tool-call display, diagnostics toggle) |
Package layout under app/src/main/java/com/hermes/client/:
data/ network clients, repositories, DTOs
di/ Hilt modules
domain/ app models (ChatMessage, Role, …)
ui/ one package per screen (chat, sessions, cron, usage,
messaging, models, profiles, settings, setup, admin, tools, nav, theme)
The launcher icon is an adaptive icon at app/src/main/res/mipmap-anydpi-v26/: the
Hermes Agent mascot (black line art on white, matching the desktop app). The foreground
is a PNG mipmap (mipmap-{m,h,xh,xxh,xxxh}dpi/ic_launcher_foreground.png) rather than a
vector — the source artwork is too path-dense for the runtime vector parser, which fails
to inflate it and falls back to the default system icon. The background is a solid-white
vector in app/src/main/res/drawable/.
- Android Studio (bundled JBR / JDK 21)
- Android SDK with
compileSdk36 / build-tools 36.x - A reachable Hermes gateway and credentials (a session token, or username/password for a password-protected dashboard)
Toolchain is pinned in gradle/libs.versions.toml: AGP 8.13.2, Kotlin 2.2.21,
Gradle 8.14.5. minSdk 26, targetSdk 36. These are the newest mutually-compatible
versions — AGP 9.x's built-in Kotlin integration is incompatible with applying the standalone
kotlin-android Kotlin Gradle plugin this project uses, and Hilt 2.59 requires AGP 9.x, so the
set is held here on purpose.
The root build.gradle.kts forces patched versions of a few transitive build-classpath
dependencies (gRPC/Google-Cloud tooling the Android Gradle Plugin bundles — netty,
protobuf, jose4j, jdom2, bouncycastle) to close known advisories. These are build-time only
and never shipped in the APK; the forces touch no app runtime dependency.
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./gradlew :app:assembleDebug
# → app/build/outputs/apk/debug/app-debug.apkRelease signing reads a gitignored keystore.properties at the repo root:
storeFile=keystore/hermes-release.jks
storePassword=********
keyAlias=hermes
keyPassword=********If that file is absent (fresh clone, CI without secrets) the release build still succeeds but is left unsigned. To create a keystore:
keytool -genkeypair -v \
-keystore keystore/hermes-release.jks \
-alias hermes -keyalg RSA -keysize 2048 -validity 10000Then build:
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./gradlew :app:assembleRelease
# → app/build/outputs/apk/release/app-release.apkThe keystore, keystore.properties, and all *.apk/*.aab/*.jks artifacts are
gitignored — signing secrets never enter the repo.
adb install -r app/build/outputs/apk/release/app-release.apkOn first launch the app shows a Setup screen. You can change any of these later from Settings → Server & token (URL, token, or username/password), then Save & reconnect.
- Gateway URL — where your Hermes dashboard is reachable from the phone, e.g.
http://100.x.x.x:9119(its Tailscale address) orhttp://192.168.x.x:9119on the same Wi-Fi. See Connecting below. - Auth — depends on how the dashboard is bound:
- Token mode — a dashboard bound to loopback (
--host 127.0.0.1) accepts the session token (sent asX-Hermes-Session-Token). Leave username/password blank. See Getting your token. - Password mode (gated) — a dashboard bound to a network address (e.g. its
Tailscale IP) refuses the loopback token and requires an auth provider, so set
Username + Password. The app logs in (
POST /auth/password-login), holds the rotating session cookie, and mints a per-connection WebSocket ticket automatically. Leave the token blank.
- Token mode — a dashboard bound to loopback (
Credentials are stored encrypted on the device. On an expired/invalid session (HTTP 401) the app re-authenticates automatically in password mode, or routes back to Setup in token mode.
For a loopback dashboard, the app authenticates with the dashboard session token. By default the gateway mints a new random token every time it starts, which is awkward to copy. For a stable token, set it yourself before starting the dashboard:
export HERMES_DASHBOARD_SESSION_TOKEN="choose-a-long-random-string"
# then start the Hermes dashboard / web gateway as usualUse that value as the Token in Setup. Treat it like a password — anyone with the token and network access to the gateway can drive your agent.
Recent Hermes refuses to expose the dashboard on a non-loopback address without a configured
auth provider. Enable the basic-auth provider in ~/.hermes/config.yaml
(dashboard.basic_auth.username + password_hash), bind the dashboard to your private
address, then enter that username/password in the app. The app handles the cookie/refresh and
WebSocket-ticket flow for you.
📖 Full Hermes installation, gateway, and token docs: hermes-agent.nousresearch.com/docs
Reach the gateway from your phone over Tailscale (recommended — works from anywhere), another VPN that puts the phone and gateway on the same private network (e.g. a self-hosted WireGuard or ZeroTier mesh), or your local network (LAN) when both are on the same Wi-Fi.
Note: this means a mesh / private VPN that actually routes to your gateway. A commercial exit VPN (e.g. Proton VPN, Mullvad) only tunnels your traffic to the public internet and cannot reach a private gateway — it's not a substitute for a private network connection.
./gradlew :app:testDebugUnitTest # JVM unit tests
./gradlew :app:connectedDebugAndroidTest # instrumented (device/emulator)Branches
| Branch | Role | Distribution |
|---|---|---|
main |
Production / stable. Always releasable. | Stable releases (v1.2.3) |
develop |
Beta / integration. New features land here first. | Beta pre-releases (v1.2.3-beta.1) |
feature/* |
Short-lived work branches. | — (open a PR into develop) |
Day-to-day flow:
feature/x ──PR──▶ develop ──(stabilize)──▶ main
│ │
beta pre-release stable release
v0.2.0-beta.1 v0.2.0
- Branch from
develop:git switch develop && git switch -c feature/my-change. - Open a PR into
develop. CI (.github/workflows/ci.yml) builds and unit-tests every push/PR. - Cut a beta for testers: tag a commit on
develop, e.g.git tag v0.2.0-beta.1 && git push --tags. - When stable, merge
develop→mainand tag the release:git tag v0.2.0 && git push --tags.
Automated releases (.github/workflows/release.yml) — pushing a v* tag builds a
signed APK and publishes a GitHub Release:
vX.Y.Z→ production release (assembleRelease), shown as Latest.vX.Y.Z-beta.N→ beta pre-release (assembleBeta), not marked Latest, soreleases/latestkeeps pointing at the stable build.
The beta build installs side-by-side with production: it has a separate application id
(com.hermes.client.beta) and the label "Hermes Beta", so testers can keep the stable
app and try betas without losing it. Anyone who wants the newest changes grabs the latest
pre-release APK from the Releases page;
everyone else gets the stable latest release.
CI signing secrets — the release workflow signs in CI using these repository secrets (Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
KEYSTORE_BASE64 |
base64 -i keystore/hermes-release.jks (the whole keystore, base64-encoded) |
KEYSTORE_PASSWORD |
keystore (store) password |
KEY_ALIAS |
key alias (e.g. hermes) |
KEY_PASSWORD |
key password |
Until those are set, tag-triggered builds will fail to sign — build/release locally with
./gradlew :app:assembleRelease (or :app:assembleBeta) instead, which reads the
gitignored keystore.properties.
Contributions are welcome — see CONTRIBUTING.md for setup, the
architecture/test conventions, and the branch workflow (new work lands on develop via PR).
All participation is governed by the Code of Conduct.
Found a vulnerability? Please report it privately — see SECURITY.md for how to report and the app's security posture (encrypted credential storage, the token/basic-auth modes, the private-network/cleartext design, and supply-chain hygiene).
If Hermes for Android is useful to you, please consider supporting development:
Every contribution helps fund new features and ongoing maintenance. GitHub Sponsors is coming soon — until then, Ko-fi is the best way to chip in. Thank you! 🙏
GNU General Public License v3.0. You are free to use, study, share, and modify this software under the terms of the GPL-3.0 — see the LICENSE file for the full text.
Hermes Agent and the Hermes mascot are property of their respective owners; this is an independent, unofficial Android client.



