v0.21.5

Hermes Agent v0.21.5


Overview

v0.21.5 — another tag that exists for downstream consumers. Released September 24, 2026. Since v0.21.4: 1,610 non-merge commits · 4,828 changed files (+164,132 / −149,440) · 460 merged PRs · 475 closed issues, measured at commit f97608f178d1ffeca59860195ab7da295f7c8e5f.

The tag is there for the same reason v0.21.4 was: Docker images, Hermes Cloud and hosted deployments all build from the newest release tag, so the fixes in this window have to land in a stable version before cloud users can get them. The release body says it plainly — the full curated write-up is deferred to v0.22.0, which will document everything from v0.21.0 onward: highlights, feature areas and complete contributor credits. Nothing in this window is skipped.

Until then, this page records who the tag is for, and the changes in the window that are already running on your machine.


Highlights

1. Desktop gets full French, German and Spanish catalogs, plus a text direction setting

Desktop now ships complete interface catalogs for French, German and Spanish, listed under Settings → Appearance → Language. The three catalogs translate every key in en.ts, including all defineFieldCopy settings labels and descriptions and the display-only intro rotation (intro-{fr,de,es}.ts). They register as defineLocale() overlays like ja, ar and ru, so a key added to English later falls back to English instead of failing typecheck for every contributor. The backend already ships locales/{fr,de,es}.yaml; display.language: fr|de|es, and regional aliases such as fr-CA, de-AT and es-419, now resolve to the matching catalog instead of falling back to English. French uses vous, German uses Sie, Spanish uses the neutral imperative tú. Proper nouns and technical identifiers (Hermes, MCP, API, OAuth, model IDs, env vars, file names) stay untranslated. PRs #120829.

Alongside it, Appearance → Chat display → Text direction with three options: Auto / Right-to-left / Left-to-right. Until now each chat prose block and each composer line picked its base direction from its first strong character (UAX#9 plaintext), which cannot tell an Arabic paragraph that opens with a Latin name from an English paragraph quoting Arabic. Auto is exactly the old behavior — no new attribute, no storage key; picking RTL or LTR applies one dir to chat prose and both composers, every line of a multi-line draft included. Code stays LTR (fenced and inline code and KaTeX remain isolated) and app chrome (sidebar, toolbar, composer buttons) does not flip. It is presentation only: no bidi control characters go into drafts, sent messages or copied text. The preference is renderer-owned, not a config.yaml key. PRs #120822.

2. Webhook deliveries can mirror into the target chat’s session

By default a delivery is fire-and-forget: each event runs in its own session, so replying “so he’s out?” in that chat gets you “who are you referring to?”. A route can now opt in with mirror_to_session: true (or hermes webhook subscribe … --mirror-to-session), and after a successful send the delivered text is appended to the target chat’s session as [Webhook delivery: <route>] plus the message, as a labelled user turn with user authority — the same path and role convention as continuable cron briefs (gateway.mirror.mirror_to_session). Off by default; only a real boolean true opts in. The mirror runs inside _profile_scope(profile), so a /p/<profile>/ route writes into that profile’s state.db and never another profile’s. It is best-effort: it never fails the delivery, and it is skipped when the chat has no gateway session yet. On a deliver_only route the mirrored text is the raw rendered payload, so enable it only for sources you trust to sit in your conversation. PRs #120825.

3. Bot Screen reaches hosted images through -desktop tags

Hosted and immutable deployments now get Bot Screen by pulling a -desktop image tag. The published image ships without Xvnc/Xfce and cannot install at run time (unprivileged hermes, no sudo, sealed /opt/hermes), so the image layer is the only delivery path. :latest / :main / :v* are the image they are today; :latest-desktop / :main-desktop / :v*-desktop add the packages and Playwright’s headed Chromium. One manifest per variant, so a desktop publish failure cannot skip slim’s :latest. Containers have no logind, so XDG_RUNTIME_DIR=/tmp/hermes-runtime is seeded 0700 as hermes at boot — the old $HOME/.cache fallback landed on the shared /opt/data volume, where two instances contended for one display lock — and a foreign-owned or symlinked directory is refused rather than chown’d. PRs #120914; the feature doc is website/docs/user-guide/features/bot-screen.md.

4. Add a model by slug from every model picker

The composer model menu, the switch-model dialog, Settings → Model and the edit-models list can all add a model by slug now: a new Add custom model… footer row turns the search box into slug entry, offering a row per configured provider as you type, and the remembered slug stays in the list as a normal row with a remove ×. PRs #120693.

5. Local voices stop reloading on every wake-up

tts.keep_warm_seconds (default 60) is new: after the last lease is released, the unload waits that long, and any toggle turning speech back on inside the window cancels it and reuses the loaded model. 0 unloads immediately. This matters most for wake-only voice conversations, which end right after their ~2s Piper warm-up — the voice used to be evicted milliseconds after it loaded, so every wake paid the full reload. PRs #121080.

6. The desktop voice loop starts reading your config

Read Aloud no longer speaks English placeholders like “code block omitted” and “link”, and no longer drops Markdown tables: code blocks and URLs are silent, MEDIA: file-link tokens never reach the voice (punctuation right after an inline token is kept), a line-final colon left hanging by a removed block becomes a period, and tables speak their header row (“Item, Value, Notes.”) while skipping the body rows. PRs #120752. Spoken stop now reads voice.stop_phrases (default ["stop"]) — it previously matched a hardcoded English list, so a Russian user saying “отбой” as the notice instructed had it sent to the agent as a prompt — and the whole utterance has to equal a phrase, with [] disabling spoken stop. Barge-in playback no longer uses a hardcoded 0.14 trigger, which sat above a Bluetooth HFP headset’s speech level while a reply plays, so talking over the reply never interrupted it; it now uses the existing voice.barge_in_threshold_multiplier (default 3.0). PRs #120751.

7. Function-key voice shortcuts are rebindable

One-shot composer dictation is now an unbound composer.dictate keyboard action, separate from voice conversation, reusing Desktop’s canonical capture path for F-keys, Caps Lock, Delete, Space and modifier combinations. The stale Desktop voice.record_key field is removed, with a note that the config stays CLI/TUI-only. Electron arbitration means an assigned F12 reaches the renderer while an unassigned F12 keeps DevTools behavior. PRs #120928. In the same window, function-key workflows on macOS were fixed: the TUI parser keeps F1–F12 terminal sequences intact (including modified-key protocol paths), and Ctrl+R was added as the portable F7 fallback for the live-work dock across the TUI and classic CLI. PRs #120666.

8. A batch of failures that only ever hit localized Windows

Four independent Windows-only problems:

  • The zone-name crash. Windows hands back zone names in the ANSI code page (Paris, Madrid (heure d'été) as cp1252 bytes). When LC_CTYPE is UTF-8 those bytes are decoded with surrogateescape, so tzname() ends up holding lone surrogates; datetime.strftime splices tzname() into the format as UTF-8, so %Z raises UnicodeEncodeError inside strftime — before any string exists that the existing _sanitize_surrogates pass could clean. The system prompt is rebuilt at every conversation start and every compaction, so the failure kept coming back. The new hermes_time.safe_strftime() is byte-identical to strftime on healthy locales (the prefix-cache invariant holds) and decodes surrogate-escaped bytes through the ANSI code page otherwise. PRs #120748.
  • Child-process decoding on Chinese Windows. subprocess.communicate()’s _readerthread on Windows decodes child output with subprocess._text_encoding() — locale.getencoding(), cp936/GBK on Chinese Windows — whenever a capture passes text=True with no encoding=. The children in question write UTF-8, so any byte illegal in GBK raises UnicodeDecodeError in that thread, which the serve process’s threading.excepthook prints as a [gateway-crash] line. hermes_cli/web_routers/files.py::_fs_git_branch was one of these (its kwargs sit in a dict literal, so the line-based text=True rule never matched it) and Desktop polls it continuously. PRs #120749.
  • Windows absolute paths. C:\...\report.html and UNC paths \\server\share\... were neither recognised as paths (so no artifact tag appeared) nor left alone by the renderer fallback, which treated anything not starting with / as relative and joined it onto cwd, producing paths like /repo/C:\...; the preview label then parsed C: as a URL scheme. The new isWindowsAbsolutePath accepts a drive letter with either slash and a UNC prefix. PRs #121074.
  • The cold-start boot probe. On a Windows cold start the local backend answers HTTP, then its event loop stalls for 12–28s while the GIL is held importing gateway platform modules. The boot-time WebSocket probe gave up after a fixed 10s, latching a boot failure, tearing the child down and landing in the “Ignoring stale Hermes backend exit (1)” cascade. A backend this app spawned now keeps waiting past the 10s base budget for as long as the child is alive, up to a 90s cap — the same budget the port-announcement step already allows for a cold start. The progress signal is liveness, not output recency, because the stall is silent. Remote gateways, the “Test remote” button and host-backend attach keep the fixed 10s budget. PRs #120750.

Improvements

  • The Desktop plugin SDK wave (composer draft API, session-list and row-decoration slots, sidebar nav prefs, model-pill labels, typed settings/skills/toolsets/profiles bridges, a sandboxed embed primitive, an appearance-settings slot, a public event bridge for plugin backends) all landed inside this window; its planned write-up is deferred too.
  • The Connectors page replaced the MCP tab, with “Connect now” for a freshly installed plugin’s MCP servers and installed plugins’ tools and skills going live in every open chat; onboarding offers catalog plugins beside connectors.
  • Kanban got a design pass: a two-column ticket modal and markdown task text.
  • The window also carries a long run of hot-path performance work across config loading, the tool registry, gateway message handling and the model picker.

Upgrade

hermes update                 # git installs
# or re-run the installer one-liner

Docker / Hermes Cloud: images build from this tag, nousresearch/hermes-agent:v2026.9.24.

Full changelog: v2026.9.21…v2026.9.24

Version mapping: the tag is date-based (v2026.9.24) and the semantic version is v0.21.5 — same release.

← Hermes Agent Changelog