{"repo":"NousResearch/hermes-agent","public_id":"45471","display_label":"Windows env variable registry fallback for HERMES_HOME","summary":"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.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":46772,"merged_at":"2026-06-15T20:16:56+00:00"},"resolution":{"state":"resolved","by":"pr-46772"},"freshness":{"state":"verified","last_verified_at":"2026-06-15T20:16:56+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":45471,"pr_number":46772,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-15T20:16:56+00:00"}],"members":[{"kind":"issue","number":45471,"title":"Windows fails to read OPENROUTER_API_KEY / HERMES_HOME","url":null,"state":"closed"},{"kind":"pr","number":45628,"title":"fix(desktop): read HERMES_HOME from the Windows registry when env is stale","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46772","number":46772}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":46772,"title":"fix(desktop): read HERMES_HOME from the Windows registry when env is stale (salvage #45628)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -39,6 +39,7 @@ const { waitForDashboardPort } = require('./backend-ready.cjs')\n const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')\n const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')\n const { buildDesktopBackendEnv, normalizeHermesHomeRoot } = require('./backend-env.cjs')\n+const { readWindowsUserEnvVar } = require('./windows-user-env.cjs')\n const { readDirForIpc } = require('./fs-read-dir.cjs')\n const { gitRootForIpc } = require('./git-root.cjs')\n const { worktreesForIpc } = require('./git-worktrees.cjs')","@@ -242,6 +243,16 @@ if (INSTALL_STAMP) {\n function resolveHermesHome() {\n   if (process.env.HERMES_HOME) return normalizeHermesHomeRoot(process.env.HERMES_HOME)\n   if (USER_DATA_OVERRIDE) return path.join(path.resolve(USER_DATA_OVERRIDE), 'hermes-home')\n+  if (IS_WINDOWS) {\n+    // A GUI app launched from Explorer inherits the environment block captured\n+    // at login, so a HERMES_HOME set via `setx` AFTER login is invisible in\n+    // process.env even though the CLI (a fresh shell) sees it. Without this the\n+    // backend silently falls back to %LOCALAPPDATA%\\hermes and reports \"No\n+    // inference provider configured\" despite a valid configured home (#45471).\n+    // Consult the live User-scoped registry value before the default below.\n+    const fromRegistry = readWindowsUserEnvVar('HERMES_HOME')\n+    if (fromRegistry) return normalizeHermesHomeRoot(fromRegistry)\n+  }\n   if (IS_WINDOWS && process.env.LOCALAPPDATA) {\n     const localappdata = path.join(process.env.LOCALAPPDATA, 'hermes')\n     const legacy = path.join(app.getPath('home'), '.hermes')"]},{"file":"apps/desktop/electron/windows-user-env.cjs","hunks":["@@ -0,0 +1,76 @@\n+// windows-user-env.cjs\n+//\n+// Read a User-scoped environment variable straight from the Windows registry\n+// (HKCU\\Environment).\n+//\n+// A GUI app launched from Explorer inherits the environment block captured at\n+// login, so a variable set via `setx` AFTER login is invisible in process.env\n+// even though a fresh shell — and the Hermes CLI — sees it immediately. The\n+// desktop's HERMES_HOME resolution relies on process.env, so that stale-snapshot\n+// gap silently sends the backend to the default %LOCALAPPDATA%\\hermes. Reading\n+// the live registry value closes the gap. See #45471.\n+\n+const { execFileSync } = require('node:child_process')\n+\n+// Parse the output of `reg query HKCU\\Environment /v <name>`, which looks like:\n+//\n+//   HKEY_CURRENT_USER\\Environment\n+//       HERMES_HOME    REG_SZ    F:\\Hermes\\data\n+//\n+// Returns the raw value string (spaces inside the value preserved), or null when\n+// the requested value line isn't present.\n+function parseRegQueryValue(stdout, name) {\n+  if (!stdout || !name) return null\n+  const typePattern =\n+    /^(\\S+)\\s+(?:REG_SZ|REG_EXPAND_SZ|REG_MULTI_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\\s+(.*)$/\n+  for (const rawLine of String(stdout).split(/\\r?\\n/)) {\n+    const line = rawLine.trim()\n+    const match = line.match(typePattern)\n+    if (match && match[1].toLowerCase() === name.toLowerCase()) {\n+      return match[2]\n+    }\n+  }\n+  return null\n+}\n+\n+// Expand %VAR% references against an env map. REG_EXPAND_SZ values store\n+// unexpanded references; plain REG_SZ paths have none, so this is a no-op for\n+// the common F:\\... case. Unknown references are left verbatim.\n+function expandWindowsEnvRefs(value, env = process.env) {\n+  if (!value) return value\n+  return value.replace(/%([^%]+)%/g, (whole, name) => {\n+    const key = Object.keys(env).find(k => k.toUpperCase() === String(name).toUpperCase())\n+    return key != null && env[key] != null ? env[key] : whole\n+  })\n+}\n+\n+// Read a User-scoped env var from HKCU\\Environment. Windows-only: returns null\n+// off-Windows (without spawning), on any spawn error, when `reg` exits non-zero\n+// (the value doesn't exist), or when the value is empty.\n+function readWindowsUserEnvVar(\n+  name,\n+  { platform = process.platform, env = process.env, exec = execFileSync } = {}\n+) {\n+  if (platform !== 'win32' || !name) return null\n+  let stdout\n+  try {\n+    stdout = exec('reg', ['query', 'HKCU\\\\Environment', '/v', name], {\n+      encoding: 'utf8',\n+      windowsHide: true,\n+      timeout: 5000\n+    })\n+  } catch {\n+    // `reg` missing, or value absent (reg exits 1) — caller falls back.\n+    return null\n+  }\n+  const raw = parseRegQueryValue(stdout, name)\n+  if (raw == null) return null\n+  const expanded = expandWindowsEnvRefs(raw, env).trim()\n+  return expanded || null\n+}\n+\n+module.exports = {\n+  expandWindowsEnvRefs,\n+  parseRegQueryValue,\n+  readWindowsUserEnvVar\n+}"]},{"file":"apps/desktop/electron/windows-user-env.test.cjs","hunks":["@@ -0,0 +1,90 @@\n+const assert = require('node:assert/strict')\n+const { test } = require('node:test')\n+\n+const {\n+  expandWindowsEnvRefs,\n+  parseRegQueryValue,\n+  readWindowsUserEnvVar\n+} = require('./windows-user-env.cjs')\n+\n+// ── parseRegQueryValue ─────────────────────────────────────────────────────\n+\n+test('parseRegQueryValue extracts a REG_SZ value', () => {\n+  const out = [\n+    '',\n+    'HKEY_CURRENT_USER\\\\Environment',\n+    '    HERMES_HOME    REG_SZ    F:\\\\Hermes\\\\data',\n+    ''\n+  ].join('\\r\\n')\n+  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), 'F:\\\\Hermes\\\\data')\n+})\n+\n+test('parseRegQueryValue matches the name case-insensitively', () => {\n+  const out = 'HKEY_CURRENT_USER\\\\Environment\\r\\n    Hermes_Home    REG_EXPAND_SZ    %USERPROFILE%\\\\h\\r\\n'\n+  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), '%USERPROFILE%\\\\h')\n+})\n+\n+test('parseRegQueryValue preserves spaces inside the value', () => {\n+  const out = '    HERMES_HOME    REG_SZ    C:\\\\Program Files\\\\Hermes\\r\\n'\n+  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), 'C:\\\\Program Files\\\\Hermes')\n+})\n+\n+test('parseRegQueryValue returns null when the value line is absent', () => {\n+  const out = 'HKEY_CURRENT_USER\\\\Environment\\r\\n    Path    REG_SZ    C:\\\\x\\r\\n'\n+  assert.equal(parseRegQueryValue(out, 'HERMES_HOME'), null)\n+  assert.equal(parseRegQueryValue('', 'HERMES_HOME'), null)\n+  assert.equal(parseRegQueryValue('garbage', 'HERMES_HOME'), null)\n+})\n+\n+// ── expandWindowsEnvRefs ───────────────────────────────────────────────────\n+\n+test('expandWindowsEnvRefs expands %VAR% case-insensitively', () => {\n+  assert.equal(\n+    expandWindowsEnvRefs('%UserProfile%\\\\h', { USERPROFILE: 'C:\\\\Users\\\\jeff' }),\n+    'C:\\\\Users\\\\jeff\\\\h'\n+  )\n+})\n+\n+test('expandWindowsEnvRefs leaves literal paths and unknown refs intact', () => {\n+  assert.equal(expandWindowsEnvRefs('F:\\\\Hermes\\\\data', {}), 'F:\\\\Hermes\\\\data')\n+  assert.equal(expandWindowsEnvRefs('%NOPE%\\\\x', {}), '%NOPE%\\\\x')\n+})\n+\n+// ── readWindowsUserEnvVar ──────────────────────────────────────────────────\n+\n+test('readWindowsUserEnvVar returns null off Windows without spawning', () => {\n+  let spawned = false\n+  const exec = () => {\n+    spawned = true\n+    return ''\n+  }\n+  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'linux', exec }), null)\n+  assert.equal(spawned, false)\n+})\n+\n+test('readWindowsUserEnvVar queries HKCU\\\\Environment and expands the value', () => {\n+  const calls = []\n+  const exec = (cmd, args) => {\n+    calls.push([cmd, args])\n+    return 'HKEY_CURRENT_USER\\\\Environment\\r\\n    HERMES_HOME    REG_EXPAND_SZ    %DRIVE%\\\\Hermes\\r\\n'\n+  }\n+  const value = readWindowsUserEnvVar('HERMES_HOME', {\n+    platform: 'win32',\n+    env: { DRIVE: 'F:' },\n+    exec\n+  })\n+  assert.equal(value, 'F:\\\\Hermes')\n+  assert.deepEqual(calls, [['reg', ['query', 'HKCU\\\\Environment', '/v', 'HERMES_HOME']]])\n+})\n+\n+test('readWindowsUserEnvVar returns null when reg exits non-zero (value missing)', () => {\n+  const exec = () => {\n+    throw new Error('reg exited 1')\n+  }\n+  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'win32', exec }), null)\n+})\n+\n+test('readWindowsUserEnvVar returns null for an empty value', () => {\n+  const exec = () => '    HERMES_HOME    REG_SZ    \\r\\n'\n+  assert.equal(readWindowsUserEnvVar('HERMES_HOME', { platform: 'win32', exec }), null)\n+})"]},{"file":"apps/desktop/package.json","hunks":["@@ -36,7 +36,7 @@\n     \"test:desktop:nsis\": \"node scripts/test-desktop.mjs nsis\",\n     \"test:desktop:existing\": \"node scripts/test-desktop.mjs existing\",\n     \"test:desktop:fresh\": \"node scripts/test-desktop.mjs fresh\",\n-    \"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\",\n+    \"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\",\n     \"typecheck\": \"tsc -p . --noEmit\",\n     \"lint\": \"eslint src/ electron/\",\n     \"lint:fix\": \"eslint src/ electron/ --fix\",\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}