# NousResearch/hermes-agent · Windows env variable registry fallback for HERMES_HOME

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #46772. Verify it addresses your case.
- **Recommended:** read PR #46772 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-46772`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #45471: Windows fails to read OPENROUTER_API_KEY / HERMES_HOME

## Prior & competing PRs

- **#46772: fix(desktop): read HERMES_HOME from the Windows registry when env is stale (salvage #45628)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - 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`
  - fix hunks:
  - `apps/desktop/electron/main.cjs`

    ```diff
    @@ -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')
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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",
    ```
- **#45628: fix(desktop): read HERMES_HOME from the Windows registry when env is stale**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#46772`

## Fix status

- Merged to main in PR #46772 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-15`.
