HermesIAssistant

Windows desktop AI assistant (Tauri 2, Rust, Svelte 5): local “Hey Hermes” wake word, voice, chat, a coding agent and consent-based access to PC folders, connected to my self-hosted Hermes agent.

Overview

Hermes Agent, the open source agent from Nous Research, runs in my homelab. I wanted it on Windows without a browser: call it by voice without touching a key, write to it from any application. HermesIAssistant is that client: a Tauri 2 application with a Svelte 5 interface in TypeScript, a Rust engine and a listening program in Python. The brain stays on the server, and the app holds no API key. CHANGELOG.md describes each version, from 1.0.0 on 29 September 2026 to 1.20.0.

The presence during a voice call simulated in the browser preview: the orb, the “Hermes parle” state, the spoken request, the start of the reply and the buttons to show the screen, mute the mic and hang up.

An overlay, a Studio and a Rust engine

The on-screen presence is a transparent overlay, always on top and non-focusable: it never steals focus from a game, and clicks pass through it except on the orb. The Studio gathers the conversations, Hermes's editable memory and the settings.

The Studio home, with the preview's demo data: conversations, memory and projects on the left, action buttons under the input, and at the bottom the status of Hermes, the voice and the wake word.

The Rust engine applies the settings itself: the interface only writes reglages.json, and appliquer_reglages applies it. Requests to the homelab go through a single nginx entry point, which adds the keys on the server side; its self-signed certificate is embedded in the app (client_entree). Behind it, a memory service in Python (memoire-api.py) serves Hermes's wiki and the relays.

“Hey Hermes” with nothing sent before the word

The wake word runs locally with Vosk and its small French model. The recognizer is restricted to a grammar (grammaire.py): call phrases (“Hey”, “Ok”, “Dis”…) followed by “Hermès”, plus decoys such as “hermine” or “Thermos”. Without them, anything that sounded a little similar became “Hermès”.

I tuned the thresholds on a test bench (eveil/banc/): 300 synthetic recordings, 180 of them trap phrases. The first prototype's setting gave 26 false alarms out of 180. With the decoys, the “call phrase right before Hermès” rule and the chosen threshold (confidence 0.63, duration 0.30 s), 3 remain. When the PC plays sound through its speakers, the criterion gets stricter: the engine reads the Windows output level, without capturing any audio.

Files that come back, folders on loan

Since 1.20.0, when Hermes creates a file, it ends its answer with a Fichier rendu : … line, which the app replaces with a card: Preview, Open, Save as. The server only serves two folders, checked after resolving links (fichier_permis). A Word preview goes through mammoth, then through assainir: an allowlist of tags, no attributes except embedded images and cell merging, links reduced to text.

With Cowork, I add a folder from the PC to a conversation: Hermes can list it, read files in it and, with my consent, write to it. I pick it in a Windows dialog opened by Rust; Hermes only knows its name. Its request goes through a skill, a relay protected by a token (hmac.compare_digest) and a long poll from the app. Rust checks every relative path (simplified excerpt of composants):

if c.starts_with(['/', '\\']) || c.contains(':') {
    return Err(CHEMIN_INVALIDE);
}
let c = c.trim_end_matches(['/', '\\']);
for e in c.split(['/', '\\']) {
    if e.is_empty() || e == "." || e == ".." {
        return Err(CHEMIN_INVALIDE);
    }
    if nom_reserve(e) {
        return Err(NOM_RESERVE);
    }
}

chemin_sur then rejects symbolic links, junctions, .lnk shortcuts and 8.3 short names. A drive root, Windows, AppData or the whole profile cannot be opened. Each write goes through a consent card: Allow, Deny, or Always in this folder (for the conversation, until the app closes). The previous version is copied into .hermes-anciennes-versions, then the write is atomic. Nothing is executed or deleted.

A coding agent behind a filter

Hermes Code (1.17 to 1.19) brings over the tools of Claude Code: modes, plan, background tasks, review. The engine is OpenAI Codex 0.160.0 in app-server mode (JSON-RPC). The app checks the size and SHA-256 hash of every engine file (FICHIERS_MOTEUR). This engine only talks to a local relay, protected by a token drawn at each launch.

Here the page is treated as untrusted. Rust only forwards 23 protocol methods, with allowed keys per method (code_moteur_regles.rs). It accepts an approval response only for a request still pending, and only once. “Ignore permissions” is confirmed in a Windows dialog, which a compromised page cannot imitate.

Real test of the approval responses, Codex engine in Manual mode: the regle-toujours line is added, the next write is refused and Hermes does not retry it.

The server side, on Linux in the homelab

Hermes Agent runs in my homelab, in a Debian LXC container on Proxmox. The agent is started there as a Docker container, restarted automatically, capped in memory and CPU, with its data on a persistent volume. The memory service is a systemd unit that starts after the network and Docker, and comes back on its own if it stops. The setup script runs with set -euo pipefail and umask 077: secret files are created with chmod 600 and never printed.

Updates go through a script run from the Proxmox host. It pushes each file into the container with its permissions (pct push), writes tokens atomically, then tests the configuration with nginx -t. If the test fails, the previous site is put back and nothing is reloaded; otherwise nginx is reloaded and the memory service restarted. In front, the nginx entry only accepts machines on the local network and the VPN, and checks the certificate of every service it relays (proxy_ssl_verify).

Testing, publishing, and what remains

The modules isolate their pure rules (*_regles.rs, src/lib/), tested without launching the app: 62 Node test files, cargo test --lib (310 tests at 1.19), unittest for the listener and the server. In a browser, pont.ts simulates the engine to check the screens. publier.ps1 refuses a version that does not go up or a modified repository, signs the installer, uploads the manifest last and sets a tag. In a test, a fake update signed for another version was rejected.

A Hermes Code session in the browser preview, with the engine simulated by pont.ts: summary of changed files, the style.css diff and the dev server terminal (local addresses masked).

I ran this project with an AI coding agent: I set the need, chose from mockups, validated the real-world tests. From 1.17 to 1.19, each version followed a spec, a plan, then a security review by a separate agent. These reviews found real defects: in Plan mode, a write could have been granted by the automatic reviewer without me. Limits remain: approvals are still given in the page, and older commands trust it. The server deployment script for 1.20.0 has not been run yet. If I started over, I would treat the page as untrusted from 1.0.