NousResearch/hermes-agent

Windows env variable registry fallback for HERMES_HOME

Cluster 45471. Data as of

Nothing open on GitHub. Fix merged in #46772 on 15 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #46772. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #46772. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address a Windows-specific issue where HERMES_HOME (and related env vars like OPENROUTER_API_KEY) may not be read correctly from the environment, with proposed fixes to fall back to the Windows registry.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #46772 fix(desktop): read HERMES_HOME from the Windows registry when env is stale (salvage #45628)

    merged to main

    files: apps/desktop/electron/main.cjs, apps/desktop/electron/windows-user-env.cjs, apps/desktop/electron/windows-user-env.test.cjs, apps/desktop/package.json

    Show the diff
    • apps/desktop/electron/main.cjs
      @@ -39,6 +39,7 @@ const { waitForDashboardPort } = require('./backend-ready.cjs')
       const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
       const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
       const { buildDesktopBackendEnv, normalizeHermesHomeRoot } = require('./backend-env.cjs')
      +const { readWindowsUserEnvVar } = require('./windows-user-env.cjs')
       const { readDirForIpc } = require('./fs-read-dir.cjs')
       const { gitRootForIpc } = require('./git-root.cjs')
       const { worktreesForIpc } = require('./git-worktrees.cjs')
      @@ -242,6 +243,16 @@ if (INSTALL_STAMP) {
       function resolveHermesHome() {
         if (process.env.HERMES_HOME) return normalizeHermesHomeRoot(process.env.HERMES_HOME)
         if (USER_DATA_OVERRIDE) return path.join(path.resolve(USER_DATA_OVERRIDE), 'hermes-home')
      +  if (IS_WINDOWS) {
      +    // A GUI app launched from Explorer inherits the environment block captured
      +    // at login, so a HERMES_HOME set via `setx` AFTER login is invisible in
      +    // process.env even though the CLI (a fresh shell) sees it. Without this the
      +    // backend silently falls back to %LOCALAPPDATA%\hermes and reports "No
      +    // inference provider configured" despite a valid configured home (#45471).
      +    // Consult the live User-scoped registry value before the default below.
      +    const fromRegistry = readWindowsUserEnvVar('HERMES_HOME')
      +    if (fromRegistry) return normalizeHermesHomeRoot(fromRegistry)
      +  }
         if (IS_WINDOWS && process.env.LOCALAPPDATA) {
           const localappdata = path.join(process.env.LOCALAPPDATA, 'hermes')
           const legacy = path.join(app.getPath('home'), '.hermes')
    • apps/desktop/electron/windows-user-env.cjs
      @@ -0,0 +1,76 @@
      +// windows-user-env.cjs
      +//
      +// Read a User-scoped environment variable straight from the Windows registry
      +// (HKCU\Environment).
      +//
      +// A GUI app launched from Explorer inherits the environment block captured at
      +// login, so a variable set via `setx` AFTER login is invisible in process.env
      +// even though a fresh shell — and the Hermes CLI — sees it immediately. The
      +// desktop's HERMES_HOME resolution relies on process.env, so that stale-snapshot
      +// gap silently sends the backend to the default %LOCALAPPDATA%\hermes. Reading
      +// the live registry value closes the gap. See #45471.
      +
      +const { execFileSync } = require('node:child_process')
      +
      +// Parse the output of `reg query HKCU\Environment /v <name>`, which looks like:
      +//
      +//   HKEY_CURRENT_USER\Environment
      +//       HERMES_HOME    REG_SZ    F:\Hermes\data
      +//
      +// Returns the raw value string (spaces inside the value preserved), or null when
      +// the requested value line isn't present.
      +function parseRegQueryValue(stdout, name) {
      +  if (!stdout || !name) return null
      +  const typePattern =
      +    /^(\S+)\s+(?:REG_SZ|REG_EXPAND_SZ|REG_MULTI_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\s+(.*)$/
      +  for (const rawLine of String(stdout).split(/\r?\n/)) {
      +    const line = rawLine.trim()
      +    const match = line.match(typePattern)
      +    if (match && match[1].toLowerCase() === name.toLowerCase()) {
      +      return match[2]
      +    }
      +  }
      +  return null
      +}
      +
      +// Expand %VAR% references against an env map. REG_EXPAND_SZ values store
      +// unexpanded references; plain REG_SZ paths have none, so this is a no-op for
      +// the common F:\... case. Unknown references are left verbatim.
      +function expandWindowsEnvRefs(value, env = process.env) {
      +  if (!value) return value
      +  return value.replace(/%([^%]+)%/g, (whole, name) => {
      +    const key = Object.keys(env).find(k => k.toUpperCase() === String(name).toUpperCase())
      +    return key != null && env[key] != null ? env[key] : whole
      +  })
      +}
      +
      +// Read a User-scoped env var from HKCU\Environment. Windows-only: returns null
      +// off-Windows (without spawning), on any spawn error, when `reg` exits non-zero
      +// (the value doesn't exist), or when the value is empty.
      +function readWindowsUserEnvVar(
      +  name,
      +  { platform = process.platform, env = process.env, exec = execFileSync } = {}
      +) {
      +  if (platform !== 'win32' || !name) return null
      +  let stdout
      +  try {
      +    stdout = exec('reg', ['query', 'HKCU\\Environment', '/v', name], {
      +      encoding: 'utf8',
      +      windowsHide: true,
      +      timeout: 5000
      +    })
      +  } catch {
      +    // `reg` missing, or value absent (reg exits 1) — caller falls back.
      +    return null
      +  }
      +  const raw = parseRegQueryValue(stdout, name)
      +  if (raw == null) return null
      +  const expanded = expandWindowsEnvRefs(raw, env).trim()
      +  return expanded || null
      +}
      +
      +module.exports = {
      +  expandWindowsEnvRefs,
      +  parseRegQueryValue,
      +  readWindowsUserEnvVar
      +}
    • apps/desktop/electron/windows-user-env.test.cjs
      @@ -0,0 +1,90 @@
      +const assert = require('node:assert/strict')
      +const { test } = require('node:test')
      +
      +const {
      +  expandWindowsEnvRefs,
      +  parseRegQueryValue,
      +  readWindowsUserEnvVar
      +} = require('./windows-user-env.cjs')
      +
      +// ── parseRegQueryValue ─────────────────────────────────────────────────────
      +
      +test('parseRegQueryValue extracts a REG_SZ value', () => {
      +  const out = [
      +    '',
      +    'HKEY_CURRENT_USER\\Environment',
      +    '    HERMES_HOME    REG_SZ    F:\\Hermes\\data',
      +    ''
      +  ].join('\r\n')
      +  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), 'F:\\Hermes\\data')
      +})
      +
      +test('parseRegQueryValue matches the name case-insensitively', () => {
      +  const out = 'HKEY_CURRENT_USER\\Environment\r\n    Hermes_Home    REG_EXPAND_SZ    %USERPROFILE%\\h\r\n'
      +  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), '%USERPROFILE%\\h')
      +})
      +
      +test('parseRegQueryValue preserves spaces inside the value', () => {
      +  const out = '    HERMES_HOME    REG_SZ    C:\\Program Files\\Hermes\r\n'
      +  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), 'C:\\Program Files\\Hermes')
      +})
      +
      +test('parseRegQueryValue returns null when the value line is absent', () => {
      +  const out = 'HKEY_CURRENT_USER\\Environment\r\n    Path    REG_SZ    C:\\x\r\n'
      +  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), null)
      +  assert.equal(parseRegQueryValue('', 'HERMES_HOME'), null)
      +  assert.equal(parseRegQueryValue('garbage', 'HERMES_HOME'), null)
      +})
      +
      +// ── expandWindowsEnvRefs ───────────────────────────────────────────────────
      +
      +test('expandWindowsEnvRefs expands %VAR% case-insensitively', () => {
      +  assert.equal(
      +    expandWindowsEnvRefs('%UserProfile%\\h', { USERPROFILE: 'C:\\Users\\jeff' }),
      +    'C:\\Users\\jeff\\h'
      +  )
      +})
      +
      +test('expandWindowsEnvRefs leaves literal paths and unknown refs intact', () => {
      +  assert.equal(expandWindowsEnvRefs('F:\\Hermes\\data', {}), 'F:\\Hermes\\data')
      +  assert.equal(expandWindowsEnvRefs('%NOPE%\\x', {}), '%NOPE%\\x')
      +})
      +
      +// ── readWindowsUserEnvVar ──────────────────────────────────────────────────
      +
      +test('readWindowsUserEnvVar returns null off Windows without spawning', () => {
      +  let spawned = false
      +  const exec = () => {
      +    spawned = true
      +    return ''
      +  }
      +  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'linux', exec }), null)
      +  assert.equal(spawned, false)
      +})
      +
      +test('readWindowsUserEnvVar queries HKCU\\Environment and expands the value', () => {
      +  const calls = []
      +  const exec = (cmd, args) => {
      +    calls.push([cmd, args])
      +    return 'HKEY_CURRENT_USER\\Environment\r\n    HERMES_HOME    REG_EXPAND_SZ    %DRIVE%\\Hermes\r\n'
      +  }
      +  const value = readWindowsUserEnvVar('HERMES_HOME', {
      +    platform: 'win32',
      +    env: { DRIVE: 'F:' },
      +    exec
      +  })
      +  assert.equal(value, 'F:\\Hermes')
      +  assert.deepEqual(calls, [['reg', ['query', 'HKCU\\Environment', '/v', 'HERMES_HOME']]])
      +})
      +
      +test('readWindowsUserEnvVar returns null when reg exits non-zero (value missing)', () => {
      +  const exec = () => {
      +    throw new Error('reg exited 1')
      +  }
      +  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'win32', exec }), null)
      +})
      +
      +test('readWindowsUserEnvVar returns null for an empty value', () => {
      +  const exec = () => '    HERMES_HOME    REG_SZ    \r\n'
      +  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'win32', exec }), null)
      +})
    • apps/desktop/package.json
      @@ -36,7 +36,7 @@
           "test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
           "test:desktop:existing": "node scripts/test-desktop.mjs existing",
           "test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
      -    "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs",
      +    "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/windows-user-env.test.cjs",
           "typecheck": "tsc -p . --noEmit",
           "lint": "eslint src/ electron/",
           "lint:fix": "eslint src/ electron/ --fix",

1 PR closed without merging

From the close comments: merged via another change 1

  • #45628 fix(desktop): read HERMES_HOME from the Windows registry when env is stale · merged via another change → #46772
Closed issues · 1
  • Closed#45471Windows fails to read OPENROUTER_API_KEY / HERMES_HOME
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#46772
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
15 Jun 2026

For your agent

The same facts, typed.