Files
hermes-agent/apps/desktop
emozilla c8c8c53a0c feat(desktop): NSIS prereq detection page + auto-install via winget
The packaged Windows installer now detects Python 3.11+ and Git for Windows
at install time and offers to install missing prereqs via winget. Mirrors
the prereq logic scripts/install.ps1 already runs for CLI installs, so
desktop installer users get the same out-of-the-box experience as
install.ps1 users.

Why
- Hermes' terminal tool calls bash.exe directly (tools/environments/
  local.py); on Windows that's Git Bash from Git for Windows. Without it,
  the agent fails on the first terminal() call.
- Hermes' Python runtime needs 3.11+. Without it, the desktop bootstrapper
  errors out at venv creation.
- Both gaps surfaced on a fresh Windows 11 VM smoke test: VM had Python
  pre-installed but no Git, so the agent's first terminal call failed
  with "Git Bash isn't installed."
- install.ps1 has had Install-Git + Install-Uv functions for ages. The
  desktop installer was the asymmetric outlier.

How — NSIS prereq page
- New file: apps/desktop/installer/prereq-check.nsh (plugged into
  electron-builder via build.nsis.include)
- Real Wizard page using nsDialogs, inserted via customPageAfterChangeDir
  hook (between the Directory page and InstFiles).
  - Group boxes for Python and Git, each showing detection status.
  - Pre-checked install checkboxes when winget is available.
  - Auto-skips silently if both prereqs are already installed.
  - Falls back to manual download URLs when winget itself is missing.
- Detection:
  - Python: probes `py -3.11`/`-3.12`/`-3.13`/`-3.14` via the Python
    launcher. Microsoft Store "Python stub" (no py.exe) is correctly
    classified as not-installed.
  - Git: `where git`.
  - winget: `where winget` (Win10 1809+ / Win11 with App Installer).
- Install execution (in customInstall macro):
  - Python: nsExec::ExecToLog with `--scope user --silent`. Per-user
    install, no UAC prompt, output streams to install log.
  - Git: ExecShellWait via Windows ShellExecute. Critical because Git
    always installs per-machine and triggers UAC; ShellExecute preserves
    the foreground focus chain across non-elevated → elevated process
    spawns, so UAC actually comes to the foreground. nsExec::ExecToLog
    breaks the chain because winget runs hidden.
  - Both pass `--disable-interactivity --accept-package-agreements
    --accept-source-agreements` to suppress winget's own dialogs.
- Verification: probes Git's standard install locations via FileExists
  rather than `where git`. NSIS's process inherits PATH at startup, so
  a freshly-installed Git won't be visible to `where` until restart.
- Silent installs (/S) skip the prompts; managed deploys handle prereqs
  out-of-band via Group Policy / Intune.

How — Electron-side safety net
- New findGitBash() in main.cjs, parallel to findSystemPython(). Probes
  the same locations as tools/environments/local.py:_find_bash() so a
  positive result here means the agent's terminal tool will work.
- ensureRuntime now throws a clear, actionable error on Windows when Git
  Bash isn't found, matching the existing "Python 3.11+ is required"
  error path.
- Catches users the NSIS page doesn't: .msi installer users (NSIS prereq
  page doesn't run for MSI), `npm run dev` users, manual installers,
  anyone who unchecked the install boxes on the NSIS prereq page.
- All gated on `IS_WINDOWS`; macOS / Linux unaffected.

NSIS build issue (resolved)
- electron-builder defaults to `-WX` (warnings as errors). NSIS optimizer
  emits "warning 6010: function not referenced" for our page functions
  because Page custom directives don't count as references in its
  static-analysis pass. The functions ARE called at runtime when NSIS
  invokes the page; the optimizer just can't see it statically.
- Set `build.nsis.warningsAsErrors=false` in package.json so this
  spurious warning doesn't fail the build. (Documented option from
  electron-builder's nsisOptions.)

Out of scope (filed for future work)
- MSI prereq detection: Windows Installer custom actions are a different
  mechanism. Enterprise deploys typically handle prereqs via GP/Intune.
- Bundle PortableGit + python-build-standalone in extraResources for
  zero-network installs. ~80MB increase.
- Mac / Linux GUI prereq flows (different installer formats; Xcode CLT
  covers most macOS prereqs already; Linux is per-distro hard).

Files
- apps/desktop/installer/prereq-check.nsh   (new, ~290 lines NSIS)
- apps/desktop/package.json                 (build.nsis.include +
                                              warningsAsErrors)
- apps/desktop/electron/main.cjs            (findGitBash + preflight)
- apps/desktop/README.md                    (Runtime prerequisites
                                              section)

Cross-platform impact
- macOS / Linux builds (dist:mac, dist:mac:dmg, dist:mac:zip): nsis
  config is ignored entirely; .nsh is dormant.
- npm run dev: .nsh dormant; main.cjs preflight gated on IS_WINDOWS.
- scripts/install.ps1, scripts/install.sh: no reference to any new
  files; CLI install paths untouched.
- Hermes CLI / dashboard / gateway: no reference; runtime untouched.
- All checks: node --check on main.cjs and test-desktop.mjs pass;
  npm run test:desktop:platforms 4/4 passing; node --test green.

Tested
- npm run dist:win produces signed .exe and .msi without errors.
- Fresh Win11 VM (Python pre-installed, no Git): prereq page renders,
  Python check shows detected, Git checkbox pre-checked. Click Next →
  Git installs via winget with UAC prompt in foreground.
- After install completes, Hermes launches and the agent's terminal
  tool can run bash commands. Verified Git Bash is detected at
  `C:\Program Files\Git\bin\bash.exe` by ensureRuntime's preflight.
2026-05-11 11:13:49 -04:00
..
2026-05-01 22:20:05 -05:00
2026-05-04 22:19:16 -05:00

Hermes Desktop

Native Electron shell for Hermes. It packages the desktop renderer, a bundled Hermes source payload, and installer targets for macOS and Windows.

Setup

Install workspace dependencies from the repo root so apps/desktop, apps/dashboard, and apps/shared stay linked:

npm install

For Python, you have two options:

Option A — let the desktop provision it for you (recommended for first-time setup): just run npm run dev. On first launch the desktop creates a venv at HERMES_HOME/hermes-agent/venv and runs pip install -e . against the resolved Hermes source automatically. Requires Python 3.11+ on PATH.

Option B — share an existing CLI install: if you already ran scripts/install.ps1 / scripts/install.sh, that's the same layout the desktop uses. The desktop reuses your existing venv and editable install — no extra steps. See Runtime Bootstrap below for details.

If you're hacking on Hermes from a clone outside HERMES_HOME/hermes-agent, point the desktop at it explicitly:

HERMES_DESKTOP_HERMES_ROOT=/path/to/your/clone npm run dev

Runtime prerequisites

Hermes Desktop needs:

  • Python 3.11+ — for the agent runtime, dashboard backend, and tool execution.
  • Git for Windows (Windows only) — provides Git Bash, which Hermes' terminal tool calls directly. Linux and macOS already ship a system bash.

The packaged Windows installer (Hermes-*.exe) detects both at install time. If either is missing it offers to install them via winget install -e --id Python.Python.3.11 and winget install -e --id Git.Git. If winget isn't available the installer shows manual download URLs and lets you continue. The MSI installer (Hermes-*.msi) doesn't run the prereq page — enterprise deploys are expected to handle prereqs out-of-band.

For dev (npm run dev) the same checks happen at first launch via the Electron bootstrapper, which throws a clear error if either prereq is missing. Manual install commands you can run yourself:

winget install -e --id Python.Python.3.11
winget install -e --id Git.Git

Development

cd apps/desktop
npm run dev

npm run dev starts Vite on 127.0.0.1:5174, launches Electron, and lets Electron boot the Hermes dashboard backend on an open port in 9120-9199. This path is for UI iteration and may still show Electron/dev identities in OS prompts.

Useful overrides:

HERMES_DESKTOP_HERMES_ROOT=/path/to/hermes-agent npm run dev
HERMES_DESKTOP_PYTHON=/path/to/python npm run dev
HERMES_DESKTOP_CWD=/path/to/project npm run dev
HERMES_DESKTOP_IGNORE_EXISTING=1 npm run dev
HERMES_HOME=/tmp/throwaway-hermes-home npm run dev
HERMES_DESKTOP_BOOT_FAKE=1 npm run dev
HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=900 npm run dev

HERMES_DESKTOP_IGNORE_EXISTING=1 skips any hermes CLI already on PATH, which is useful when testing the factory-image bootstrap path.

HERMES_HOME overrides the install root (default: %LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere) — handy for sandboxed dev runs that shouldn't touch your real config.

HERMES_DESKTOP_BOOT_FAKE=1 adds deterministic per-phase delays to desktop startup so you can validate the startup overlay and progress bar. For convenience, npm run dev:fake-boot enables fake mode with defaults.

On a fresh Hermes profile, Desktop shows a first-run setup overlay after boot. The overlay saves the minimum required provider credential (for example OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY) to the active Hermes .env, reloads the backend env, and then lets the user continue without opening Settings manually.

Dashboard Dev

Run the Python dashboard backend with embedded chat enabled:

hermes dashboard --tui --no-open

For dashboard HMR, start Vite in another terminal:

cd apps/dashboard
npm run dev

Open the Vite URL. The dev server proxies /api, /api/pty, and plugin assets to http://127.0.0.1:9119 and fetches the live dashboard HTML so the ephemeral session token matches the running backend.

Build

npm run build
npm run pack          # unpacked app at release/mac-<arch>/Hermes.app
npm run dist:mac      # macOS DMG + zip
npm run dist:mac:dmg  # DMG only
npm run dist:mac:zip  # zip only
npm run dist:win      # NSIS + MSI

Before packaging, stage:hermes copies the Python Hermes payload into build/hermes-agent. Electron Builder then ships it as Contents/Resources/hermes-agent.

Automated Releases

Desktop installers are published by .github/workflows/desktop-release.yml with two channels:

  • Stable: runs on published GitHub releases and uploads signed artifacts to that release tag.
  • Nightly: runs on main pushes and updates the rolling desktop-nightly prerelease.

The workflow injects a channel-aware desktop version at build time:

  • stable: derived from the release tag (for example v2026.5.5 -> 2026.5.5)
  • nightly: 0.0.0-nightly.YYYYMMDD.<sha>

Artifact names include channel, platform, and architecture:

Hermes-<version>-<channel>-<platform>-<arch>.<ext>

Each run also publishes SHA256SUMS-<platform>.txt so installers can be verified.

Stable release gates

Stable builds fail fast if signing credentials are missing:

  • macOS signing + notarization: CSC_LINK, CSC_KEY_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER
  • Windows signing: WIN_CSC_LINK, WIN_CSC_KEY_PASSWORD

Stable macOS builds also validate stapling and Gatekeeper assessment in CI before upload.

Icons

Desktop icons live in assets/:

  • assets/icon.icns
  • assets/icon.ico
  • assets/icon.png

The builder config points at assets/icon. Replace these files directly if the app icon changes.

Testing Install Paths

Use the package-local test scripts from this directory:

npm run test:desktop:all
npm run test:desktop:existing
npm run test:desktop:fresh
npm run test:desktop:dmg
npm run test:desktop:platforms

test:desktop:existing builds the packaged app and opens it normally. It should use an existing hermes CLI if one is on PATH, preserving the users real ~/.hermes config.

test:desktop:fresh builds the packaged app and launches it in a throwaway fresh-install sandbox. It sets HERMES_DESKTOP_IGNORE_EXISTING=1, points Electron userData at a temp dir, points HERMES_HOME at a temp dir, and launches through the factory-image bootstrap path without touching your real desktop runtime or ~/.hermes.

test:desktop:dmg builds and opens the DMG.

test:desktop:platforms runs platform bootstrap-path assertions, including:

  • existing-CLI vs factory-image runtime path selection semantics
  • WSL2 protection against Windows .exe/.cmd/.bat/.ps1 overrides
  • platform-specific runtime import checks (winpty vs ptyprocess)

For fast reruns without rebuilding:

HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:fresh
HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:existing
HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:dmg

Installing Locally

npm run dist:mac:dmg
open release/Hermes-0.0.0-arm64.dmg

Drag Hermes to Applications. If testing repeated installs, replace the existing app.

Runtime Bootstrap

Hermes Desktop shares its install layout with the CLI installers (scripts/install.ps1, scripts/install.sh) so a desktop-only user and a CLI-only user end up with the same files in the same places.

Where things live

HERMES_HOME/                       # %LOCALAPPDATA%\hermes (Windows)
                                   # ~/.hermes (macOS / Linux)
├── hermes-agent/                  # ACTIVE_HERMES_ROOT — the canonical install
│   ├── hermes_cli/, agent/, ...   # Python source
│   ├── pyproject.toml             # source of truth for deps
│   ├── venv/                      # virtualenv (Scripts\python.exe on Windows,
│   │                              #             bin/python elsewhere)
│   └── .hermes-desktop-runtime.json   # marker: schema version + pyproject hash
├── config.yaml                    # user config
├── .env                           # API keys
└── logs/
    ├── desktop.log                # Electron-side boot log
    ├── agent.log
    ├── errors.log
    └── gateway.log

The factory image (Contents/Resources/hermes-agent on macOS, resources\hermes-agent on Windows) ships inside the .app / .exe and seeds HERMES_HOME/hermes-agent on first launch.

Resolution order

The desktop resolves a Hermes backend in this order:

  1. HERMES_DESKTOP_HERMES_ROOT — explicit dev override.
  2. Existing hermes CLI on PATH (skipped when HERMES_DESKTOP_IGNORE_EXISTING=1).
  3. Repo source root — only when running npm run dev from a checkout. Takes precedence over HERMES_HOME/hermes-agent so devs always run their local edits.
  4. HERMES_HOME/hermes-agent if it already exists (CLI installer or prior desktop launch).
  5. Packaged + factory image present → sync factory → HERMES_HOME/hermes-agent, then use it.
  6. Pip-installed hermes_cli module via system Python.

First-launch flow on a packaged install

  1. Sync factory image → HERMES_HOME/hermes-agent. Skipped if a .git directory exists at the destination (developer install) — never overwrites a user's local repo.
  2. Create venv at HERMES_HOME/hermes-agent/venv using system Python (errors out with a Python-install hint if no Python 3.11+ is found).
  3. pip install -e HERMES_HOME/hermes-agentpyproject.toml is the single source of truth for dependencies.
  4. Stamp .hermes-desktop-runtime.json with the schema version + pyproject hash + factory version.

Subsequent launches compare the marker against the active pyproject.toml and skip steps 2-4 when nothing has changed.

Upgrades

A new installer drops a new factory image. On next launch the marker mismatches → factory contents are copied over HERMES_HOME/hermes-agent (excluding venv/, .git, __pycache__, etc.), pip install -e re-runs to pick up new deps, the marker is re-stamped. The venv is preserved across upgrades to keep the upgrade fast when deps haven't moved.

A user who installed via scripts/install.ps1 / scripts/install.sh (so HERMES_HOME/hermes-agent/.git exists) is detected as a developer install and the desktop never overwrites their checkout — they keep using hermes update / git pull to update.

Debugging

Desktop boot logs are written to:

HERMES_HOME/logs/desktop.log     # %LOCALAPPDATA%\hermes\logs\desktop.log on Windows
                                  # ~/.hermes/logs/desktop.log on macOS / Linux

If the UI reports Desktop boot failed, check that log first. It includes the backend command output and recent Python traceback context.

To reset desktop runtime state (forces re-sync from the factory image and re-pip install -e . on next launch):

# macOS / Linux
rm "$HOME/.hermes/hermes-agent/.hermes-desktop-runtime.json"

# Windows (PowerShell)
Remove-Item "$env:LOCALAPPDATA\hermes\hermes-agent\.hermes-desktop-runtime.json"

For a full reset of just the Python venv (rare — usually only needed if the venv is broken):

# macOS / Linux
rm -rf "$HOME/.hermes/hermes-agent/venv"

# Windows (PowerShell)
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\hermes\hermes-agent\venv"

To reset stale macOS microphone permission prompts:

tccutil reset Microphone com.github.Electron
tccutil reset Microphone com.nousresearch.hermes

Verification

Run before handing off installer changes:

npm run fix
npm run type-check
npm run lint
npm run test:desktop:all

Current lint may report existing warnings, but it should exit with no errors.