{"repo":"NousResearch/hermes-agent","public_id":"41385","display_label":"macOS desktop app missing PATH entries for tools","summary":"Multiple bugs and fixes addressing the macOS desktop/GUI app inheriting a minimal launchd PATH that omits directories like ~/.local/bin and ~/.hermes/node/bin, causing tools such as computer_use, codex, and cua-driver to be undiscoverable. Proposed fixes involve extending or resolving the login-shell PATH for GUI-launched backend processes.","subsystem":"agent","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":3,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":44814},"resolution":{"state":"partially_resolved","by":"pr-69696,pr-69902"},"freshness":{"state":"verified","last_verified_at":"2026-08-17T05:05:51+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.18666666666666665},"fix_edges":[{"issue_number":41385,"pr_number":44814,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":42923,"pr_number":69696,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":44813,"pr_number":44814,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":48158,"pr_number":69696,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":51249,"pr_number":69696,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":51393,"pr_number":69902,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":41385,"title":"[Bug]: /codex-runtime on fails on macOS desktop — dashboard inherits minimal PATH without ~/.hermes/node/bin","url":null,"state":"closed"},{"kind":"issue","number":51249,"title":"[Bug]: Hermes Desktop backend PATH misses ~/.local/bin","url":null,"state":"closed"},{"kind":"issue","number":51393,"title":"computer_use tool silently unavailable in macOS desktop app (works in CLI) — which() gate misses ~/.local/bin under launchd PATH","url":null,"state":"closed"},{"kind":"pr","number":41389,"title":"fix(desktop): extend PATH for dashboard backend so codex is discoverable on macOS","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#44814","number":44814},{"kind":"pr","value":"#41385","number":41385}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":44814,"title":"fix(desktop): complete backend PATH for Homebrew Codex","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/backend-env.cjs","apps/desktop/electron/backend-env.test.cjs","apps/desktop/electron/main.cjs","apps/desktop/package.json"],"fix_hunks":[{"file":"apps/desktop/electron/backend-env.cjs","hunks":["@@ -0,0 +1,101 @@\n+const path = require('node:path')\n+\n+// Match the POSIX fallback surface used by the Python terminal environment.\n+// macOS apps launched from Finder/Dock often inherit only /usr/bin:/bin:/usr/sbin:/sbin,\n+// which misses Apple Silicon Homebrew and user-installed CLI tools such as codex.\n+const POSIX_SANE_PATH_ENTRIES = Object.freeze([\n+  '/opt/homebrew/bin',\n+  '/opt/homebrew/sbin',\n+  '/usr/local/sbin',\n+  '/usr/local/bin',\n+  '/usr/sbin',\n+  '/usr/bin',\n+  '/sbin',\n+  '/bin'\n+])\n+\n+function delimiterForPlatform(platform = process.platform) {\n+  return platform === 'win32' ? ';' : ':'\n+}\n+\n+function pathModuleForPlatform(platform = process.platform) {\n+  return platform === 'win32' ? path.win32 : path.posix\n+}\n+\n+function pathEnvKey(env = process.env, platform = process.platform) {\n+  if (platform !== 'win32') return 'PATH'\n+  return Object.keys(env || {}).find(key => key.toUpperCase() === 'PATH') || 'PATH'\n+}\n+\n+function currentPathValue(env = process.env, platform = process.platform) {\n+  const key = pathEnvKey(env, platform)\n+  return env?.[key] || ''\n+}\n+\n+function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {\n+  const seen = new Set()\n+  const ordered = []\n+\n+  for (const entry of entries) {\n+    if (!entry) continue\n+    const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)\n+    for (const part of parts) {\n+      if (!part || seen.has(part)) continue\n+      seen.add(part)\n+      ordered.push(part)\n+    }\n+  }\n+\n+  return ordered.join(delimiter)\n+}\n+\n+function buildDesktopBackendPath({\n+  hermesHome,\n+  venvRoot,\n+  currentPath = '',\n+  platform = process.platform,\n+  pathModule = pathModuleForPlatform(platform)\n+} = {}) {\n+  const delimiter = delimiterForPlatform(platform)\n+  const hermesNodeBin = hermesHome ? pathModule.join(hermesHome, 'node', 'bin') : null\n+  const venvBin = venvRoot ? pathModule.join(venvRoot, platform === 'win32' ? 'Scripts' : 'bin') : null\n+  const saneEntries = platform === 'win32' ? [] : POSIX_SANE_PATH_ENTRIES\n+\n+  return appendUniquePathEntries(\n+    [hermesNodeBin, venvBin, currentPath, saneEntries],\n+    { delimiter }\n+  )\n+}\n+\n+function buildDesktopBackendEnv({\n+  hermesHome,\n+  pythonPathEntries = [],\n+  venvRoot,\n+  currentEnv = process.env,\n+  platform = process.platform,\n+  pathModule = pathModuleForPlatform(platform)\n+} = {}) {\n+  const delimiter = delimiterForPlatform(platform)\n+  const currentPythonPath = currentEnv?.PYTHONPATH || ''\n+  const key = pathEnvKey(currentEnv, platform)\n+\n+  return {\n+    PYTHONPATH: appendUniquePathEntries([...pythonPathEntries, currentPythonPath], { delimiter }),\n+    [key]: buildDesktopBackendPath({\n+      hermesHome,\n+      venvRoot,\n+      currentPath: currentPathValue(currentEnv, platform),\n+      platform,\n+      pathModule\n+    })\n+  }\n+}\n+\n+module.exports = {\n+  POSIX_SANE_PATH_ENTRIES,\n+  appendUniquePathEntries,\n+  buildDesktopBackendEnv,\n+  buildDesktopBackendPath,\n+  delimiterForPlatform,\n+  pathEnvKey\n+}"]},{"file":"apps/desktop/electron/backend-env.test.cjs","hunks":["@@ -0,0 +1,95 @@\n+const test = require('node:test')\n+const assert = require('node:assert/strict')\n+const path = require('node:path')\n+\n+const {\n+  POSIX_SANE_PATH_ENTRIES,\n+  appendUniquePathEntries,\n+  buildDesktopBackendEnv,\n+  buildDesktopBackendPath,\n+  pathEnvKey\n+} = require('./backend-env.cjs')\n+\n+test('desktop backend PATH adds Hermes-managed bins and missing POSIX sane entries', () => {\n+  const result = buildDesktopBackendPath({\n+    hermesHome: '/Users/test/.hermes',\n+    venvRoot: '/Users/test/.hermes/hermes-agent/venv',\n+    currentPath: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',\n+    platform: 'darwin',\n+    pathModule: path.posix\n+  })\n+\n+  const entries = result.split(':')\n+  assert.equal(entries[0], '/Users/test/.hermes/node/bin')\n+  assert.equal(entries[1], '/Users/test/.hermes/hermes-agent/venv/bin')\n+  assert.ok(entries.includes('/opt/homebrew/bin'), 'Apple Silicon Homebrew bin is added')\n+  assert.ok(entries.includes('/opt/homebrew/sbin'), 'Apple Silicon Homebrew sbin is added')\n+  assert.ok(entries.includes('/usr/local/sbin'), 'missing standard sbin is added')\n+\n+  for (const expected of POSIX_SANE_PATH_ENTRIES) {\n+    assert.ok(entries.includes(expected), `${expected} should be present`)\n+  }\n+})\n+\n+test('desktop backend PATH preserves first occurrence and avoids duplicates', () => {\n+  const result = buildDesktopBackendPath({\n+    hermesHome: '/Users/test/.hermes',\n+    venvRoot: '/Users/test/.hermes/hermes-agent/venv',\n+    currentPath: '/opt/homebrew/bin:/usr/bin:/opt/homebrew/bin:/bin',\n+    platform: 'darwin',\n+    pathModule: path.posix\n+  })\n+\n+  const entries = result.split(':')\n+  assert.equal(entries.filter(entry => entry === '/opt/homebrew/bin').length, 1)\n+  assert.ok(\n+    entries.indexOf('/opt/homebrew/bin') < entries.indexOf('/opt/homebrew/sbin'),\n+    'existing Homebrew bin keeps its precedence over appended missing sane entries'\n+  )\n+})\n+\n+test('buildDesktopBackendEnv extends PYTHONPATH and backend PATH together', () => {\n+  const env = buildDesktopBackendEnv({\n+    hermesHome: '/Users/test/.hermes',\n+    pythonPathEntries: ['/repo/hermes-agent'],\n+    venvRoot: '/Users/test/.hermes/hermes-agent/venv',\n+    currentEnv: {\n+      PATH: '/usr/bin:/bin',\n+      PYTHONPATH: '/existing/pythonpath'\n+    },\n+    platform: 'darwin',\n+    pathModule: path.posix\n+  })\n+\n+  assert.equal(env.PYTHONPATH, '/repo/hermes-agent:/existing/pythonpath')\n+  assert.ok(env.PATH.startsWith('/Users/test/.hermes/node/bin:/Users/test/.hermes/hermes-agent/venv/bin:'))\n+  assert.ok(env.PATH.includes('/opt/homebrew/bin'))\n+})\n+\n+test('Windows PATH casing and delimiter are preserved without POSIX sane entries', () => {\n+  const env = buildDesktopBackendEnv({\n+    hermesHome: 'C:\\\\Users\\\\test\\\\AppData\\\\Local\\\\hermes',\n+    pythonPathEntries: ['C:\\\\repo\\\\hermes-agent'],\n+    venvRoot: 'C:\\\\Users\\\\test\\\\AppData\\\\Local\\\\hermes\\\\hermes-agent\\\\venv',\n+    currentEnv: {\n+      Path: 'C:\\\\Windows\\\\System32;C:\\\\Windows',\n+      PYTHONPATH: 'C:\\\\existing\\\\pythonpath'\n+    },\n+    platform: 'win32',\n+    pathModule: path.win32\n+  })\n+\n+  assert.equal(pathEnvKey({ Path: 'x' }, 'win32'), 'Path')\n+  assert.equal(env.PATH, undefined)\n+  assert.ok(env.Path.startsWith('C:\\\\Users\\\\test\\\\AppData\\\\Local\\\\hermes\\\\node\\\\bin;'))\n+  assert.ok(env.Path.includes('\\\\venv\\\\Scripts;'))\n+  assert.ok(env.Path.includes(';C:\\\\Windows\\\\System32;C:\\\\Windows'))\n+  assert.equal(env.Path.includes('/opt/homebrew/bin'), false)\n+})\n+\n+test('appendUniquePathEntries drops empty entries and keeps first occurrence', () => {\n+  assert.equal(\n+    appendUniquePathEntries([':/a::/b', ['/a', '/c']], { delimiter: ':' }),\n+    '/a:/b:/c'\n+  )\n+})"]},{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -33,6 +33,7 @@ const { adoptServedDashboardToken } = require('./dashboard-token.cjs')\n const { PortPool } = require('./port-pool.cjs')\n const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')\n const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')\n+const { buildDesktopBackendEnv } = require('./backend-env.cjs')\n const { readDirForIpc } = require('./fs-read-dir.cjs')\n const { gitRootForIpc } = require('./git-root.cjs')\n const {","@@ -2134,9 +2135,11 @@ function createPythonBackend(root, label, dashboardArgs, options = {}) {\n     label,\n     command: python,\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n-    env: {\n-      PYTHONPATH: [root, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter)\n-    },\n+    env: buildDesktopBackendEnv({\n+      hermesHome: HERMES_HOME,\n+      pythonPathEntries: [root],\n+      venvRoot: path.join(root, 'venv')\n+    }),\n     root,\n     bootstrap: Boolean(options.bootstrap),\n     shell: false","@@ -2155,9 +2158,11 @@ function createActiveBackend(dashboardArgs) {\n     label: `Hermes at ${ACTIVE_HERMES_ROOT}`,\n     command: fileExists(venvPython) ? venvPython : findSystemPython(),\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n-    env: {\n-      PYTHONPATH: [ACTIVE_HERMES_ROOT, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter)\n-    },\n+    env: buildDesktopBackendEnv({\n+      hermesHome: HERMES_HOME,\n+      pythonPathEntries: [ACTIVE_HERMES_ROOT],\n+      venvRoot: VENV_ROOT\n+    }),\n     root: ACTIVE_HERMES_ROOT,\n     bootstrap: true,\n     shell: false"]},{"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-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/port-pool.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/port-pool.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     \"typecheck\": \"tsc -p . --noEmit\",\n     \"lint\": \"eslint src/ electron/\",\n     \"lint:fix\": \"eslint src/ electron/ --fix\",\n"]}]},{"kind":"pr","number":51251,"title":"fix(desktop): include user local bin in backend PATH","url":null,"state":"closed","close_reason":{"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":51648,"title":"fix(computer_use): resolve cua-driver in canonical install dirs under thin GUI PATH (#51393)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#69902","number":69902}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":69696,"title":"feat(desktop): resolve login-shell PATH for GUI launches (port of cline/cline#12429)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main.ts","apps/desktop/electron/shell-path.test.ts","apps/desktop/electron/shell-path.ts"]},{"kind":"pr","number":69902,"title":"fix(computer_use): resolve cua-driver under thin GUI PATH (supersedes #55631)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","hermes_cli/main.py","hermes_cli/tools_config.py","tests/computer_use/test_cua_cli_fallback_env.py","tests/computer_use/test_cua_spawn_env_sanitization.py","tests/computer_use/test_doctor.py","tests/computer_use/test_permissions_resolution.py","tests/hermes_cli/test_tools_config.py","tests/tools/test_computer_use.py","tools/computer_use/cua_backend.py","tools/computer_use/doctor.py","tools/computer_use/permissions.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}