Hermes Desktop Fixes
Bugs Fixed
0. LC_CTYPE Locale Warning (Cosmetic, Annoying)
Symptom: bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory — appears 6× in the Desktop in-app terminal on every shell start.
Root cause: Electron inherits macOS-style LC_CTYPE=UTF-8 (bare, no language prefix like en_US.). Bash calls setlocale() during startup before any profile/rc file is sourced, so .bashrc fixes are too late.
Fix: Three layers (belt-and-suspenders):
~/.hermes/.env—LC_CTYPE=en_US.UTF-8(loaded byload_hermes_dotenv()before spawning terminals)~/.hermes/hermes-env.sh— conditional export whenLC_CTYPEis “UTF-8” or empty~/.config/environment.d/50-locale-fix.conf— systemd user env (applies to new login sessions) Note: The.bash_profileearly-export approach does NOT work — bash’ssetlocale()fires before any profile is sourced.
1. Provider Identity Loss (Critical)
Symptom: “No LLM provider configured” on new Desktop sessions; CLI works fine.
Root cause: _session_info() in tui_gateway/server.py reports agent.provider = "custom" (resolved generic) instead of "custom:litellm-vertex-ai". Desktop caches this bare "custom" in localStorage, sends it on next session.create. _get_named_custom_provider("custom") returns None → falls through → no provider.
Fix: _resolve_session_info_provider(agent) helper maps bare "custom" back to custom:<name> via find_custom_provider_identity(base_url).
2. Goose Node Wrapper Breaks Builds
Symptom: npm/electron-builder fails with CWD errors.
Root cause: /usr/lib/Goose/resources/bin/node wrapper does cd ~/.config/goose/mcp-hermit before running real node.
Fix: hermes-env.sh strips it from PATH.
3. Electron Sandbox Requires Sudo (RETIRED — v3.0)
Status: Patch retired as of Sep 2026. Upstream refactored _desktop_linux_sandbox_fixup() — the function still exists but the code structure changed, so the sed pattern no longer matches. The ELECTRON_DISABLE_SANDBOX=1 env var in hermes-env.sh and .env still prevents the sudo prompt; the source patch is no longer needed or applied.
Original symptom: hermes desktop prompts for sudo password to set SUID on chrome-sandbox.
Original fix: Early return when ELECTRON_DISABLE_SANDBOX=1 is set, injected via sed into main.py.
4. Generic Taskbar Icon on Fedora/GNOME Wayland (Rewritten — v3.0)
Symptom: Hermes shows a generic application icon in the GNOME taskbar/dock instead of the Hermes logo. May also show duplicate icons (one pinned, one for the running window).
Root cause (v2.1, Jul 2026): StartupWMClass=hermes (lowercase) vs Electron’s WM_CLASS=Hermes (capital H). This was the X11-era fix.
Root cause (v3.0, Sep 2026): On Wayland, GNOME matches windows to .desktop files using the app-id, not StartupWMClass. Electron derives the app-id from package.json desktopName field, which electron-builder sets via extraMetadata: { desktopName: appId } → resolves to com.nousresearch.hermes. GNOME looks for com.nousresearch.hermes.desktop — if that file doesn’t exist, no icon match → generic icon.
Additional complication: Upstream’s linux_desktop_entry.py auto-generates hermes.desktop on every hermes desktop launch with a broken Exec path (points into a stale install environment venv). If both hermes.desktop and our entry coexist, GNOME shows two entries in Apps grid.
Fix (v3.0):
- Desktop entry named
com.nousresearch.hermes.desktop— matches Electron’s Wayland app-id Icon=hermes(themed name) — resolves via upstream’s hicolor icon installs at 24/32/48/256pxStartupWMClass=com.nousresearch.hermes— matches Electron’s Wayland app-id (NOT the X11productName“Hermes”)Execuses our launcher wrapper withELECTRON_DISABLE_SANDBOX=1and--skip-build- Background watcher in launcher wrapper detects upstream’s
hermes.desktopregeneration AND upstream overwriting our entry (checks Exec line), deletes duplicates and restores correct content - GNOME favorites pinned to
com.nousresearch.hermes.desktop
Diagnostics:
# Check Electron's Wayland app-id (from built package.json)
npx asar extract ~/.hermes/hermes-agent/apps/desktop/release/linux-unpacked/resources/app.asar /tmp/asar-check
python3 -c "import json; print(json.load(open('/tmp/asar-check/package.json'))['desktopName'])"
# Check which .desktop file GNOME used to launch
cat /proc/$(pgrep -f "linux-unpacked/Hermes" | head -1)/environ 2>/dev/null | tr '\0' '\n' | grep GIO_LAUNCHED_DESKTOP_FILE
# Verify icon theme resolves
python3 -c "import gi; gi.require_version('Gtk','3.0'); from gi.repository import Gtk; i=Gtk.IconTheme.get_default().lookup_icon('hermes',48,0); print(i.get_filename() if i else 'NOT FOUND')"
# Check for duplicate entries
ls ~/.local/share/applications/*hermes*.desktop
6. GPU Process Crash on Intel Arc + Wayland (v3.2)
Symptom: Hermes Desktop starts, backend comes up healthy, but ~40s later Electron crashes with FATAL: GPU process isn't usable. Goodbye. in desktop-chromium.log. The Electron window disappears silently.
Root cause: Intel Arc (Meteor Lake-P) iGPU + Electron 40 + --no-sandbox + Wayland. The GPU subprocess fails to launch (error_code=1002 = kLaunchFailed), the zygote communication breaks, Electron retries 6×, then exits with FATAL. Occurs after kernel/mesa updates (e.g., kernel 7.2.7, mesa 26.2.3 on Fedora 44). The i915 driver + Chromium GPU process isolation don’t play well together when the sandbox is disabled.
Fix: desktop.disable_gpu: true in ~/.hermes/config.yaml → Python launcher sets HERMES_DESKTOP_DISABLE_GPU=1 in env → Electron calls app.disableHardwareAcceleration() + appends --disable-gpu-compositing before app.ready. Software rendering is used instead. The GPU FATAL still appears in the chromium log but is non-fatal because the main process no longer depends on the GPU subprocess.
Config:
hermes config set desktop.disable_gpu true
Diagnostics:
# Check for GPU crash in chromium log
grep "FATAL.*GPU" ~/.hermes/logs/desktop-chromium.log
# Verify disable_gpu is set
grep disable_gpu ~/.hermes/config.yaml
# Verify env var reaches Electron
cat /proc/$(pgrep -f "linux-unpacked/Hermes --disable" | head -1)/environ 2>/dev/null | tr '\0' '\n' | grep HERMES_DESKTOP_DISABLE_GPU
5. electron-builder Can’t Find Electron (RETIRED — v3.0)
Status: Patch retired as of Sep 2026. Upstream removed patch-electron-builder-mac-binary.cjs entirely — only dmgbuild-diagnostics.cjs remains in apps/desktop/scripts/. The electron workspace hoisting issue may have been fixed upstream or is no longer relevant with current electron-builder versions.
Original symptom: Build fails looking for ../../node_modules/electron/dist.
Original fix: Inject symlink creation into the prebuilder script.
Architecture
Update-proof mechanism
The key challenge: upstream’s linux_desktop_entry.py regenerates hermes.desktop on every hermes desktop launch, and git pull --ff-only refuses to merge when skip-worktree files differ from the index.
Solution: The launcher wrapper (~/.local/bin/hermes) handles two concerns:
Update interception:
- Pre-flight: Read-only flags (
--check,--plan,--list-venv-holders,--install-id) pass through without touching patches. Plainhermes updateruns--checkfirst; if already up to date, short-circuits (avoids upstream’s unconditional TUI/web/desktop rebuild in the completion path). - Before update:
hermes-revert-patches.shfinds all skip-worktree files (git ls-files -v | grep ^S), clears flags, and checks out clean copies — this is generic, not hardcoded, so retired patches with orphaned flags are automatically cleaned up - During update:
git pull --ff-only/git resetsucceeds (working tree is clean) - After update:
hermes-apply-patches.shre-applies patches + sets skip-worktree - Belt-and-suspenders:
post-mergegit hook also calls apply-patches
Desktop entry maintenance (background watcher):
On hermes desktop, a background subshell polls for upstream’s hermes.desktop (up to 30s). When detected:
- Deletes upstream’s
hermes.desktop(brokenExec, causes duplicate in Apps grid) - Writes/refreshes
com.nousresearch.hermes.desktopwith correctExecandIcon=hermes - Runs
update-desktop-database
Files outside git (permanent, never overwritten by update)
| File | Purpose |
|---|---|
~/.hermes/hermes-env.sh |
PATH fix + ELECTRON_DISABLE_SANDBOX=1 + LC_CTYPE fix |
~/.hermes/hermes-apply-patches.sh |
Idempotent: applies source patches + sets skip-worktree |
~/.hermes/hermes-revert-patches.sh |
Generically clears all skip-worktree flags + checks out clean upstream before update |
~/.hermes/hermes-check-patches.sh |
Health check |
~/.hermes/.env |
ELECTRON_DISABLE_SANDBOX=1 + LC_CTYPE=en_US.UTF-8 (loaded by load_hermes_dotenv()) |
~/.local/share/applications/com.nousresearch.hermes.desktop |
GNOME desktop entry (matches Wayland app-id) |
~/.local/bin/hermes |
Launcher: sources env, intercepts update, desktop entry watcher |
~/.hermes/hermes-agent/.git/hooks/post-merge |
Calls hermes-apply-patches.sh |
Files patched (git-tracked, skip-worktree protected)
tui_gateway/server.py—_resolve_session_info_provider()+ usage in_session_info()
Retired patches (no longer applied):
(upstream refactored, env var sufficient)hermes_cli/main.py— sandbox bypass(file removed upstream)apps/desktop/scripts/patch-electron-builder-mac-binary.cjs— electron symlink
Recovery
# Health check
bash ~/.hermes/hermes-check-patches.sh
# Full recovery (idempotent)
bash ~/.agents/skills/hermes-desktop-fixes/recover.sh
# Clear stale Desktop cache (if provider issue recurs)
rm -rf ~/.config/Hermes/Local\ Storage/leveldb/*
Resolved Issues (v3.1)
- Orphaned skip-worktree on
hermes_cli/main.py: The v3.0 retirement of Bug #3 stopped applying themain.pypatch but never cleared theskip-worktreeflag left behind by older versions. When upstream changedhermes_cli/main.py,git reset --hardfailed withEntry 'hermes_cli/main.py' not uptodate. Cannot merge.The revert script now generically discovers and clears all skip-worktree flags, and the health check detects orphaned flags. - GPU process crash kills Electron on Intel Arc + Wayland: Fixed via
desktop.disable_gpu: truein config.yaml — uses Hermes’s built-inHERMES_DESKTOP_DISABLE_GPUmechanism to callapp.disableHardwareAcceleration()beforeapp.ready. - Desktop entry overwritten by upstream after update: Upstream’s
linux_desktop_entry.pyoverwritescom.nousresearch.hermes.desktopwith a brokenExecpath (raw python invocation instead of our launcher wrapper). The background watcher only detectshermes.desktopcreation, not modifications to our own entry. Manually restored correct entry. - Unconditional TUI/web/desktop rebuild on every
hermes update: Upstream’s completion path (complete_source_checkout→build_update_products) unconditionally rebuilds all frontends even when already up to date. This was masked before because the orphaned skip-worktree flag made updates fail before reaching the build step. Fixed by adding a--checkpre-flight in the launcher: plainhermes updatenow fetches and checks first, short-circuiting in ~2s when current instead of ~2min of unnecessary rebuilds.
Safety Rules
- NEVER use
gdbus call org.gnome.Shell.Evalto inspect windows on Wayland. This can crash the entire GNOME session (compositor crash = full logout). There is no safe programmatic way to introspect live Wayland windows from a terminal. Verify.desktopentry correctness by reading the file and checkingpackage.json desktopName— never by poking the compositor.
Known Limitations
- If upstream renames
_session_infoor changes the provider plumbing, the sed pattern for Bug #1 will silently fail. The health check detects this. - If upstream changes
desktopNamefromcom.nousresearch.hermesto a different app-id, the desktop entry filename must be updated to match. - The background watcher polls for up to 30s. If upstream’s deferred desktop entry write takes longer (unlikely), the duplicate may briefly appear.
- The Desktop rebuild during
hermes updatecallspython -m hermes_cli.main desktop --build-onlydirectly (bypasses launcher), but--build-onlyreturns before the sandbox check, so it’s non-fatal. - The launcher wrapper uses
$HOMEinHERMES_BINpath, but upstream’s_wrapper_targets_checkout()does literal string comparison against the resolved absolute path. The probe won’t recognize our wrapper — harmless becausemanage_launcher_entry: falseprevents the desktop entry rewrite. - Upstream’s
linux_desktop_entry.pynow writes directly tocom.nousresearch.hermes.desktop(not justhermes.desktop). Themanage_launcher_entry: falseconfig opt-out is the primary defense; the launcher’s background watcher is belt-and-suspenders.
Changelog
See CHANGELOG.md for version history.