Hermes Chat UI provides a customizable interface that can run entirely on private infrastructure (e.g. NAS). It is deployed separately from the official Hermes Agent image: Hermes owns the runtime, tools, dashboard and canonical session data; this project owns the web/PWA and a minimal browser-facing backend.
- Installable Progressive Web App for desktop and mobile.
- Responsive, mobile-first interface.
- Real-time answer, reasoning, and tool-activity streaming.
- Multiple inline images, automatically resized before submission, preserved across refreshes/restarts, and expandable in a responsive lightbox.
- Model selection and per-session model persistence.
- Canonical Hermes history: sessions created by this UI, the CLI, dashboard, cron, and other integrations appear together.
- Multiple pinned chats with desktop and touch-friendly rename/delete actions.
- Paginated session list suitable for long-lived installations.
- Lazy conversation history in 30-message visual pages with anchored upward scrolling.
- Background completion and Web Push support.
Hermes Agent is the only source of truth for sessions and messages. The UI uses the official Hermes Sessions API and does not maintain a second chat database or access Hermes SQLite tables directly. Because Hermes may omit submitted image data from persisted history, the UI keeps only its own image blobs and a message-ID association index under /app/data/attachments.
This has two important consequences:
- A session created outside this UI is visible here.
- Deleting a session here permanently deletes the canonical Hermes session, so it disappears from the CLI, dashboard, cron, and every other Hermes interface. After Hermes confirms the deletion, the UI also removes that session's locally retained images.
Bulk deletion is intentionally unavailable.
graph LR
Browser[Browser / PWA] -->|HTTP :8643| Proxy[Chat UI BFF]
Proxy -->|Bearer token, internal HTTP :8642| API[Official Hermes API]
Proxy -->|read-only model defaults| Config[Hermes config.yaml]
Proxy -->|image blobs and safe ID index| UIData[UI /app/data]
API --> DB[(Hermes state database)]
API <--> Agent[Hermes Agent]
Agent <--> LLM[Local or remote LLM]
Agent <--> Tools[Tools and integrations]
The browser communicates only with the proxy on the UI origin. The proxy injects API_SERVER_KEY server-side, so the Hermes bearer token is never included in browser JavaScript. At startup, the UI checks /v1/capabilities and requires Hermes session resources, session chat, and streaming support.
Images are compressed as a group. The final request is kept below the Hermes API's approximately 10 MB request limit. If all selected images cannot fit after compression, the UI sends none of them and keeps the draft intact. Images sent after this feature is deployed are copied into the mounted UI /app/data volume; already-lost historical images cannot be recovered.
The UI image runs as UID 10001. When the Hermes configuration directory is
not traversable/readable by that UID, grant the UI process only the minimum
read access it needs; this does not make the mount writable:
sudo setfacl -m u:10001:rx /volume2/docker_ssd/hermes/config
sudo setfacl -m u:10001:r /volume2/docker_ssd/hermes/config/config.yamlUse the file-only read-only mount in the UI Compose:
- /volume2/docker_ssd/hermes/config/config.yaml:/hermes-config/config.yaml:roRepeat the file ACL command if Hermes replaces config.yaml during a
configuration migration. Do not use chmod 644 as a workaround: it would make
the configuration readable by every local account.
- Download the examples:
curl -O https://raw.githubusercontent.com/lukegskw/hermes-chat-ui/main/docker-compose.hermes-agent.example.yml
curl -O https://raw.githubusercontent.com/lukegskw/hermes-chat-ui/main/docker-compose.ui.example.yml
curl -O https://raw.githubusercontent.com/lukegskw/hermes-chat-ui/main/.env.example- Create local configuration files:
cp docker-compose.hermes-agent.example.yml docker-compose.hermes-agent.yml
cp docker-compose.ui.example.yml docker-compose.ui.yml
cp .env.example .env-
Edit
.env. At minimum, replaceAPI_SERVER_KEY=changemewith a strong random value and configure Hermes/model credentials according to the official Hermes Agent documentation.Before production, test the selected Hermes image without changing it:
HERMES_CONTRACT_URL=http://your-nas:8642 \ HERMES_CONTRACT_API_KEY="$API_SERVER_KEY" \ .venv/bin/python scripts/check-hermes-contract.py -
Start Hermes first. It creates the shared private Docker network:
docker compose -f docker-compose.hermes-agent.yml up -d- Start the UI separately:
docker compose -f docker-compose.ui.yml up -d --build- Open
http://localhost:8643. The native Hermes dashboard is available athttp://localhost:9119when enabled.
The two Compose files start independent containers joined only by the private
hermes-internal network. No file, plugin, startup hook, or project component
is added to hermes-agent; its existing /opt/data volume remains the
canonical state.
| Variable | Description | Default |
|---|---|---|
PROXY_PORT |
Host port for the UI and TypeScript BFF | 8643 |
BACKEND_PORT |
Host port for the native Hermes API | 8642 |
DASHBOARD_PORT |
Host and container port for the Hermes dashboard | 9119 |
API_SERVER_ENABLED |
Enable the Hermes native API; must remain enabled | true |
API_SERVER_KEY |
Required bearer key shared by Hermes and the UI BFF, never the browser | none |
API_SERVER_HOST |
Hermes API bind address inside the container | 0.0.0.0 |
API_SERVER_PORT |
Hermes API port inside the container | 8642 |
API_SERVER_MODEL_NAME |
Optional model name reported by the API | none |
| Variable | Description | Default |
|---|---|---|
HERMES_DASHBOARD |
Enable the official Hermes dashboard | 1 |
HERMES_DASHBOARD_BASIC_AUTH_USERNAME |
Dashboard username | none |
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD |
Dashboard password | none |
HERMES_DASHBOARD_BASIC_AUTH_SECRET |
Stable dashboard auth secret | none |
| Variable | Description | Default |
|---|---|---|
HASS_URL |
Home Assistant URL | none |
HASS_TOKEN |
Home Assistant long-lived access token | none |
GITHUB_TOKEN |
GitHub personal access token used by Hermes tools | none |
VAPID_SUBJECT |
Contact URI used for Web Push | mailto:push@example.com |
HERMES_PUSH_API_KEY |
Required dedicated bearer key for proactive-message endpoints | none |
Proactive automation creates a new canonical Hermes conversation containing the supplied final assistant text, then sends Web Push. The script talks to the separate UI container over Docker DNS; Generate a dedicated internal key:
openssl rand -hex 32Set that value as HERMES_PUSH_API_KEY for both Compose projects. The UI also
receives the same dashboard username/password already configured for Hermes,
plus HERMES_DASHBOARD_URL=http://hermes-agent:9119. These credentials stay in
the BFF and are used only to call the official session-import operation; they
are never exposed to browser JavaScript.
Keep the caller script with the Hermes-managed skill or automation that owns the notification. This repository intentionally does not install or maintain that script. The only required integration contract is one authenticated request to:
http://hermes-chat-ui:8643/api/proactive/messages
Example from the Hermes container environment:
python3 /opt/data/skills/proactive-message/notify.py \
"Backup completed successfully." "NAS backup"If session import fails, push is still attempted and its body explicitly says
that the conversation was not saved. Successful notifications link directly
to the newly imported Hermes session. The service worker persists that session
target before opening or focusing the PWA, so iOS can recover it after either a
suspended-app resume or a cold start. The UI keeps bounded request-id records in
/app/data/proactive_requests.json so an ordinary retry cannot duplicate a
completed import or push.
This project is designed for one trusted user on a private network. The UI proxy itself is not a multi-user authentication layer.
Warning
Do not expose ports 8642, 8643, or 9119 directly to the public internet. Use Tailscale, a VPN, or a properly configured authenticated reverse proxy with TLS. Set dashboard credentials whenever the dashboard is reachable by another machine.
Keep .env out of version control, use a strong API_SERVER_KEY, and restrict access to the Hermes /opt/data and UI /app/data volumes. The example pins an image digest candidate; validate its capability contract and pin the tested digest before production.
The bundled or externally configured Hermes instance is too old or does not advertise the required capability flags. Update Hermes; this UI does not fall back to its former database.
Confirm that the UI proxy and the CLI/dashboard use the same /opt/data volume. The UI requests sessions from every source and includes child sessions. Use “Load more” when more than 50 sessions exist.
Confirm that API_SERVER_KEY is set once in .env and passed to the container. The browser should never be configured with this key.
Check container logs and verify that API_SERVER_ENABLED=true and API_SERVER_PORT matches the container-side API port. In the split Compose topology the UI connects to http://hermes-agent:8642 on the private Docker network.
This warning is emitted by Hermes itself, not by the UI BFF. A session row has
messages but no cached assembled system prompt, so Hermes rebuilds that prompt,
continues the turn, and attempts to persist it with update_system_prompt.
The immediate effect is a prefix-cache miss for that turn, not lost chat
history. The UI sends every message in a chat to the same canonical session and
does not write Hermes' internal assembled prompt.
One warning when an older session is first resumed can therefore self-heal. If
the same session ID warns on every turn, inspect adjacent Hermes logs for
Session DB update_system_prompt failed; that indicates an upstream database
write/path problem. This client deliberately does not patch that private field
or add code to the official Hermes container.
Install the app on the Home Screen, allow notifications, and verify the iOS version supports Home Screen web-app badges. The app cannot override a system-level notification or badge preference.
Prerequisites:
- Node.js 24
- pnpm 11.22.0 (the project pin can be activated with Corepack)
- A current Hermes Agent API exposing the Sessions API
Install dependencies and start the frontend:
corepack enable
pnpm install --frozen-lockfile
pnpm run devIn another terminal, start the TypeScript BFF:
API_SERVER_KEY=your-key \
HERMES_API_URL=http://127.0.0.1:8642 \
HERMES_PROXY_PORT=8643 \
pnpm run dev:serverThe Vite app connects to the BFF on port 8643 during development. Run
pnpm test, pnpm run type-check, pnpm run lint, and pnpm run build before
submitting a change. The production image contains only Node.js, the compiled
TypeScript server, and the built SPA; Python is not installed.
Contributions are welcome. Please preserve strict TypeScript safety, test UI changes in English and Portuguese, and avoid adding a second session persistence path.
Licensed under the MIT License. See LICENSE.
