Browse by topic

Hermes yesterday, October 31, 2026: a busy gateway stops dropping every client

What landed on Hermes Agent main on October 31, 2026: a gateway that no longer stalls under many sessions, a Windows plugin-name fix that protects the Hermes folder, secret caches moved where sandboxes can hide them and quiet Telegram groups.

Arcade fighting game pixel-art card over a city at sunset; ROUND 1, 50 commits hit main, a busy gateway stops dropping clients, with a P1 CODE and P2 SHIP scoreboard.

Run two dozen sessions through one gateway and its event loop spent 94.7% of its time on chores like compressing every streamed token, until it stopped answering and dropped every client at once. Not anymore. 50 commits landed on main, almost all of them fixes, and here are the 10 that matter.

1. A busy gateway stops dropping every client at once

Before: with many sessions in one gateway, the event loop compressed every outbound socket frame (one per streamed token of every session) and re-read control state after every tool call, and on a multiplexed gateway (the default) every status poll from the CLI, the TUI and Desktop also rebuilt the profile’s secret and terminal scope on the loop. The loop then stopped answering for seconds at a time, and clients printed Gateway disconnected; turn outcome is unknown. In the PR’s profile with 24 sessions the loop was busy 94.7% of the time. After: compression is off when every peer is local (a loopback bind with no public URL or tunnel and no SSH isolation; remote setups keep it), and the control-card refresh and the per-session polls run in worker threads. In the PR’s 32-session test the worst loop stall fell from about 15 seconds to about 4, with no client failures. Limits: the per-token socket send and a few session reads still run on the loop, and the hosted-room watcher that rebuilds the scope every second is still in two open PRs.

Pull requests: Frame compression and control reads, RPC scope and session polls off the loop.

2. On Windows, a plugin name can no longer aim a removal at your whole Hermes folder

Before: the desktop plugin install and remove calls added on October 28 checked names only for slashes, leading dots and control characters, so on Windows a name like C: pointed at the desktop-plugins folder itself and C:.. at the Hermes home, and a forced remove would delete that whole tree; a forced install of a package whose manifest is named C:.. reached the same target. After: names with a colon, Windows device names such as CON, NUL or COM1 with any extension, and a trailing dot are refused on every OS. Heads-up: the bug never reached a stable release. The matching check for the Desktop app’s own native installer, which on every OS accepted a name like ../victim, merged after midnight UTC and is tomorrow’s item. The fix was tested with Windows path rules on Linux, not on a Windows machine.

Pull request: Windows plugin names outside the plugins folder.

3. Secret caches move to where a sandbox can actually hide them

Before: the Bitwarden and 1Password disk caches lived in the profile’s cache folder and the Photon sidecar record (port, token, pid) in runtime, folders the agent can write, so a sandbox that hides files by path could be bypassed by renaming the folder and reading the file at its new path (measured on the Windows MXC sandbox the OpenShell plugin uses). The 1Password cache was also missing from every credential guard: the file tools’ read and write denies, profile export and the gateway’s media-delivery deny. After: the four files live at the top of the profile home (bws_cache.json, bws_cache.enc.json, op_cache.json, photon-sidecar.json), every guard lists them, and an old copy in cache or runtime is deleted unread on first use, since it could be a link or a forged record. The cost is one extra Bitwarden or 1Password fetch after the update, and Photon goes without its record until the next gateway restart. Limit: a profile that never uses that secret source again keeps its old file in cache, still covered by every guard.

Pull request: Secret caches at the profile home root.

4. Telegram groups: a bot told to stay quiet now stays quiet

Before: Telegram never told the gateway whether a message expected a reply, so a bot told to answer [SILENT] to group chatter posted the warning that the model returned only a silence marker to the whole group, and with two bots in one group the notice could bounce between them. After: in groups, a reply to another member, a message that opens by mentioning someone else, or any other message without a question mark can end silently. DMs, a mention of the bot, a reply to it, a match of its mention patterns and slash commands keep the notice, and so do questions, uncaptioned voice notes and audio files, and replies to channel or anonymous-admin posts. Limits: Telegram only (Discord needs its own change), uncaptioned documents count as chatter, and in a forum topic the bot created itself every message counts as addressed to it.

Pull request: Telegram group messages can stay silent.

5. Docker: one container per named profile, cron and Kanban included

Before: with persistent Docker, a named profile split its work across two containers: session-less work (Kanban workers, cron) used the default container while the profile’s Desktop and TUI sessions used the profile’s own, so a file a Kanban worker wrote in /root was missing in Desktop, and the file panel could serve one session another session’s file from the same path. After: gateway chats, Desktop sessions, cron and Kanban workers of a named profile share one container and one sandbox; the default profile still uses default, and non-persistent and non-Docker backends are unchanged. Heads-up: a gateway launched as a named profile (hermes -p work gateway) moves its chats from that profile’s old default sandbox to the profile_<name> one; the old folder is kept, not migrated, so copy anything you still need.

Pull request: One Docker container per named profile.

6. Any room can opt into intentional silence

Before: in a multi-agent room, a bot whose final answer to a message addressed to it was only a silence marker (NO_REPLY or [SILENT]) had it replaced by the silence-marker warning, with no way to say silence is fine there. After: set allow_silence: true under platforms.<platform>.channel_overrides.<room> and a healthy, marker-only final stays silent in that room; leaving it out, null or false keeps today’s behavior. It also reads top-level platform blocks, a failed config lookup keeps the default rule instead of posting an error, and a failed crash-recovery receipt no longer turns the silence into an error reply. Limits: only the final message is held back (tool actions and progress already sent stay), the value must be a real boolean (the string true does nothing), a thread entry that sets other keys hides its parent room’s opt-in, and a reply written next to a housekeeping-only tool call is still delivered.

Pull request: Per-room allow_silence.

7. Bot Chat deliveries stop getting refused for good

Before: on the first delivery, a TUI or CLI Bot Chat stamped with the owner’s own login label (basic:alice, for example) was renamed aside and a new conversation started, splitting the history, although opening it first adopted it in place. A Bot Chat stamped by another OS user or an unknown login, or a compressed one whose latest segment nobody could adopt, made every delivery (a cron beat, for example) fail with not_found for good. After: owner-labelled history is adopted in place, each row is judged by who is actually delivering, rows that cannot be adopted are moved aside so the delivery lands, and a compressed chat is judged by its latest segment, the one a delivery writes to. A forwarded foreign login never moves the owner’s own chat aside; that delivery is refused. Limit: if the owner’s delivery reaches an unbound row stamped by a forwarded login first, the owner wins and that login’s later deliveries are refused, since there is one Bot Chat per profile.

Pull request: Bot Chat history on delivery.

8. Browser: raw CDP calls that never fetch a URL work again on guarded backends

Before: since a hardening change on main on October 28, raw browser_cdp calls that only name URLs (request blocklists, interception patterns, dialog answers, cookie setters) and public bracketed IPv6 URLs were refused on cloud browsers, /browser connect endpoints and Docker, SSH or Modal terminals, even a blocklist meant to keep the page away from the cloud metadata address. After: those eight methods are no longer screened as fetch targets and every other method keeps its checks, while the cloud-metadata floor now also catches IPv4-mapped IPv6, scheme-relative addresses that start with two slashes, hex, decimal and octal IPv4 spellings, and the host after a userinfo part holding a bracket or parenthesis. Limits: URLs built at run time are invisible to any literal scan, scheme-relative forms are checked for the metadata address only, userinfo holding a quote or a space still hides the host from the floor, and there was no live cloud-browser run.

Pull request: Raw CDP calls on guarded backends.

9. Desktop plugin installs stop refusing good plugins and stop calling refused ones installed

Before: on macOS and Windows a plugin whose folder is spelled Desktop was refused as a symbolic link; a repo with plugin.js at its root counted its .git toward the 64 MB cap, so a 32-64 MB plugin was refused; the host-side reconcile copied links inside an installed package’s desktop folder into the served plugins folder; and in the Webapp a refused update kept running the old code while the plugins tab said installed. After: only a real link on the path is refused, .git and __pycache__ stay out of root-layout installs and the cap, the host reconcile refuses links and applies the cap to every desktop folder (a dev checkout linked into plugins keeps its exemption), and the Webapp tab reports the refusal. Limits: copies already published with a now-refused link are not pruned, Windows junctions are not covered, and there was no live macOS or Windows run.

Pull request: Desktop plugin tree checks.

10. Windows gateways stop growing about 17 MB a day

Before: on Windows with Python 3.11 to 3.13, the update-lock check a running gateway makes every 30 seconds after any update built a new ctypes type each time, which Python keeps forever, so the gateway grew about 6 KB per check, about 17 MB a day. After: the type and the lock calls are built once. The bug came in on main on October 28 and never reached a stable release; the new test was not run on a Windows host.

Pull request: Windows update-lock memory leak.

Meanwhile, in the engine room

Dozens of fixes landed yesterday that most users never see, and the gateway and Desktop plugin installs took most of them.

  • An open Webapp tab on a secondary profile let a multiplexed scale-to-zero instance suspend under it, because the tab’s heartbeat was written in that profile’s folder while the idle watcher read the launch profile’s; both now use the launch home (pull request)
  • With All profiles grouped by profile in the Desktop sidebar, an empty profile section could land under the wrong gateway and show a populated profile twice, and a profile whose scan failed claimed No sessions yet; empty sections now follow their gateway, and a failed profile gets none (pull request)
  • The PDF skill’s OCR steps lost the Python path between terminal calls and ran an empty command, and the document skills’ sandbox fallback could delete a working environment another agent had just published; both are fixed (pull request)
  • The bundled hermes-agent skill no longer tells agents that sending text and Enter in one call fails; it does submit (pull request)

If you noticed none of this, it worked.

All on main, ships in the next release (Desktop app changes reach the Microsoft Store app with the release and the .exe and .dmg app with its next bundled release). Latest stable: v0.22.5.

Watch the recap

The 56-second motion recap, made with Remotion, is on X.

Was this configuration guide helpful?

If you have questions, encountered issues during setup, or want to discuss networking and security, connect or reach out on LinkedIn.

Connect on LinkedIn