# NousResearch/hermes-agent · macOS desktop app missing PATH entries for tools

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 9 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 3 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #44814. Verify it addresses your case.
- **Recommended:** read PR #44814 (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: `partially_resolved` · deciding PR(s): `pr-69696,pr-69902`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.19`: 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

- #41385: \[Bug\]: /codex-runtime on fails on macOS desktop — dashboard inherits minimal PATH without ~/.hermes/node/bin
- #51249: \[Bug\]: Hermes Desktop backend PATH misses ~/.local/bin
- #51393: computer_use tool silently unavailable in macOS desktop app (works in CLI) — which() gate misses ~/.local/bin under launchd PATH

## Prior & competing PRs

- **#44814: fix(desktop): complete backend PATH for Homebrew Codex**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/backend-env.cjs`, `apps/desktop/electron/backend-env.test.cjs`, `apps/desktop/electron/main.cjs`, `apps/desktop/package.json`
  - fix hunks:
  - `apps/desktop/electron/backend-env.cjs`

    ```diff
    @@ -0,0 +1,101 @@
    +const path = require('node:path')
    +
    +// Match the POSIX fallback surface used by the Python terminal environment.
    +// macOS apps launched from Finder/Dock often inherit only /usr/bin:/bin:/usr/sbin:/sbin,
    +// which misses Apple Silicon Homebrew and user-installed CLI tools such as codex.
    +const POSIX_SANE_PATH_ENTRIES = Object.freeze([
    +  '/opt/homebrew/bin',
    +  '/opt/homebrew/sbin',
    +  '/usr/local/sbin',
    +  '/usr/local/bin',
    +  '/usr/sbin',
    +  '/usr/bin',
    +  '/sbin',
    +  '/bin'
    +])
    +
    +function delimiterForPlatform(platform = process.platform) {
    +  return platform === 'win32' ? ';' : ':'
    +}
    +
    +function pathModuleForPlatform(platform = process.platform) {
    +  return platform === 'win32' ? path.win32 : path.posix
    +}
    +
    +function pathEnvKey(env = process.env, platform = process.platform) {
    +  if (platform !== 'win32') return 'PATH'
    +  return Object.keys(env || {}).find(key => key.toUpperCase() === 'PATH') || 'PATH'
    +}
    +
    +function currentPathValue(env = process.env, platform = process.platform) {
    +  const key = pathEnvKey(env, platform)
    +  return env?.[key] || ''
    +}
    +
    +function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {
    +  const seen = new Set()
    +  const ordered = []
    +
    +  for (const entry of entries) {
    +    if (!entry) continue
    +    const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)
    +    for (const part of parts) {
    +      if (!part || seen.has(part)) continue
    +      seen.add(part)
    +      ordered.push(part)
    +    }
    +  }
    +
    +  return ordered.join(delimiter)
    +}
    +
    +function buildDesktopBackendPath({
    +  hermesHome,
    +  venvRoot,
    +  currentPath = '',
    +  platform = process.platform,
    +  pathModule = pathModuleForPlatform(platform)
    +} = {}) {
    +  const delimiter = delimiterForPlatform(platform)
    +  const hermesNodeBin = hermesHome ? pathModule.join(hermesHome, 'node', 'bin') : null
    +  const venvBin = venvRoot ? pathModule.join(venvRoot, platform === 'win32' ? 'Scripts' : 'bin') : null
    +  const saneEntries = platform === 'win32' ? [] : POSIX_SANE_PATH_ENTRIES
    +
    +  return appendUniquePathEntries(
    +    [hermesNodeBin, venvBin, currentPath, saneEntries],
    +    { delimiter }
    +  )
    +}
    +
    +function buildDesktopBackendEnv({
    +  hermesHome,
    +  pythonPathEntries = [],
    +  venvRoot,
    +  currentEnv = process.env,
    +  platform = process.platform,
    +  pathModule = pathModuleForPlatform(platform)
    +} = {}) {
    +  const delimiter = delimiterForPlatform(platform)
    +  const currentPythonPath = currentEnv?.PYTHONPATH || ''
    +  const key = pathEnvKey(currentEnv, platform)
    +
    +  return {
    +    PYTHONPATH: appendUniquePathEntries([...pythonPathEntries, currentPythonPath], { delimiter }),
    +    [key]: buildDesktopBackendPath({
    +      hermesHome,
    +      venvRoot,
    +      currentPath: currentPathValue(currentEnv, platform),
    +      platform,
    +      pathModule
    +    })
    +  }
    +}
    +
    +module.exports = {
    +  POSIX_SANE_PATH_ENTRIES,
    +  appendUniquePathEntries,
    +  buildDesktopBackendEnv,
    +  buildDesktopBackendPath,
    +  delimiterForPlatform,
    +  pathEnvKey
    +}
    ```
  - `apps/desktop/electron/backend-env.test.cjs`

    ```diff
    @@ -0,0 +1,95 @@
    +const test = require('node:test')
    +const assert = require('node:assert/strict')
    +const path = require('node:path')
    +
    +const {
    +  POSIX_SANE_PATH_ENTRIES,
    +  appendUniquePathEntries,
    +  buildDesktopBackendEnv,
    +  buildDesktopBackendPath,
    +  pathEnvKey
    +} = require('./backend-env.cjs')
    +
    +test('desktop backend PATH adds Hermes-managed bins and missing POSIX sane entries', () => {
    +  const result = buildDesktopBackendPath({
    +    hermesHome: '/Users/test/.hermes',
    +    venvRoot: '/Users/test/.hermes/hermes-agent/venv',
    +    currentPath: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
    +    platform: 'darwin',
    +    pathModule: path.posix
    +  })
    +
    +  const entries = result.split(':')
    +  assert.equal(entries[0], '/Users/test/.hermes/node/bin')
    +  assert.equal(entries[1], '/Users/test/.hermes/hermes-agent/venv/bin')
    +  assert.ok(entries.includes('/opt/homebrew/bin'), 'Apple Silicon Homebrew bin is added')
    +  assert.ok(entries.includes('/opt/homebrew/sbin'), 'Apple Silicon Homebrew sbin is added')
    +  assert.ok(entries.includes('/usr/local/sbin'), 'missing standard sbin is added')
    +
    +  for (const expected of POSIX_SANE_PATH_ENTRIES) {
    +    assert.ok(entries.includes(expected), `${expected} should be present`)
    +  }
    +})
    +
    +test('desktop backend PATH preserves first occurrence and avoids duplicates', () => {
    +  const result = buildDesktopBackendPath({
    +    hermesHome: '/Users/test/.hermes',
    +    venvRoot: '/Users/test/.hermes/hermes-agent/venv',
    +    currentPath: '/opt/homebrew/bin:/usr/bin:/opt/homebrew/bin:/bin',
    +    platform: 'darwin',
    +    pathModule: path.posix
    +  })
    +
    +  const entries = result.split(':')
    +  assert.equal(entries.filter(entry => entry === '/opt/homebrew/bin').length, 1)
    +  assert.ok(
    +    entries.indexOf('/opt/homebrew/bin') < entries.indexOf('/opt/homebrew/sbin'),
    +    'existing Homebrew bin keeps its precedence over appended missing sane entries'
    +  )
    +})
    +
    +test('buildDesktopBackendEnv extends PYTHONPATH and backend PATH together', () => {
    +  const env = buildDesktopBackendEnv({
    +    hermesHome: '/Users/test/.hermes',
    +    pythonPathEntries: ['/repo/hermes-agent'],
    +    venvRoot: '/Users/test/.hermes/hermes-agent/venv',
    +    currentEnv: {
    +      PATH: '/usr/bin:/bin',
    +      PYTHONPATH: '/existing/pythonpath'
    +    },
    +    platform: 'darwin',
    +    pathModule: path.posix
    +  })
    +
    +  assert.equal(env.PYTHONPATH, '/repo/hermes-agent:/existing/pythonpath')
    +  assert.ok(env.PATH.startsWith('/Users/test/.hermes/node/bin:/Users/test/.hermes/hermes-agent/venv/bin:'))
    +  assert.ok(env.PATH.includes('/opt/homebrew/bin'))
    +})
    +
    +test('Windows PATH casing and delimiter are preserved without POSIX sane entries', () => {
    +  const env = buildDesktopBackendEnv({
    +    hermesHome: 'C:\\Users\\test\\AppData\\Local\\hermes',
    +    pythonPathEntries: ['C:\\repo\\hermes-agent'],
    +    venvRoot: 'C:\\Users\\test\\AppData\\Local\\hermes\\hermes-agent\\venv',
    +    currentEnv: {
    +      Path: 'C:\\Windows\\System32;C:\\Windows',
    +      PYTHONPATH: 'C:\\existing\\pythonpath'
    +    },
    +    platform: 'win32',
    +    pathModule: path.win32
    +  })
    +
    +  assert.equal(pathEnvKey({ Path: 'x' }, 'win32'), 'Path')
    +  assert.equal(env.PATH, undefined)
    +  assert.ok(env.Path.startsWith('C:\\Users\\test\\AppData\\Local\\hermes\\node\\bin;'))
    +  assert.ok(env.Path.includes('\\venv\\Scripts;'))
    +  assert.ok(env.Path.includes(';C:\\Windows\\System32;C:\\Windows'))
    +  assert.equal(env.Path.includes('/opt/homebrew/bin'), false)
    +})
    +
    +test('appendUniquePathEntries drops empty entries and keeps first occurrence', () => {
    +  assert.equal(
    +    appendUniquePathEntries([':/a::/b', ['/a', '/c']], { delimiter: ':' }),
    +    '/a:/b:/c'
    +  )
    +})
    ```
  - `apps/desktop/electron/main.cjs`

    ```diff
    @@ -33,6 +33,7 @@ const { adoptServedDashboardToken } = require('./dashboard-token.cjs')
     const { PortPool } = require('./port-pool.cjs')
     const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
     const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
    +const { buildDesktopBackendEnv } = require('./backend-env.cjs')
     const { readDirForIpc } = require('./fs-read-dir.cjs')
     const { gitRootForIpc } = require('./git-root.cjs')
     const {
    ```

    ```diff
    @@ -2134,9 +2135,11 @@ function createPythonBackend(root, label, dashboardArgs, options = {}) {
         label,
         command: python,
         args: ['-m', 'hermes_cli.main', ...dashboardArgs],
    -    env: {
    -      PYTHONPATH: [root, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter)
    -    },
    +    env: buildDesktopBackendEnv({
    +      hermesHome: HERMES_HOME,
    +      pythonPathEntries: [root],
    +      venvRoot: path.join(root, 'venv')
    +    }),
         root,
         bootstrap: Boolean(options.bootstrap),
         shell: false
    ```

    ```diff
    @@ -2155,9 +2158,11 @@ function createActiveBackend(dashboardArgs) {
         label: `Hermes at ${ACTIVE_HERMES_ROOT}`,
         command: fileExists(venvPython) ? venvPython : findSystemPython(),
         args: ['-m', 'hermes_cli.main', ...dashboardArgs],
    -    env: {
    -      PYTHONPATH: [ACTIVE_HERMES_ROOT, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter)
    -    },
    +    env: buildDesktopBackendEnv({
    +      hermesHome: HERMES_HOME,
    +      pythonPathEntries: [ACTIVE_HERMES_ROOT],
    +      venvRoot: VENV_ROOT
    +    }),
         root: ACTIVE_HERMES_ROOT,
         bootstrap: true,
         shell: false
    ```
  - `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-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",
    +    "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",
         "typecheck": "tsc -p . --noEmit",
         "lint": "eslint src/ electron/",
         "lint:fix": "eslint src/ electron/ --fix",
    ```
- **#69696: feat(desktop): resolve login-shell PATH for GUI launches (port of cline/cline#12429)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/main.ts`, `apps/desktop/electron/shell-path.test.ts`, `apps/desktop/electron/shell-path.ts`
- **#69902: fix(computer_use): resolve cua-driver under thin GUI PATH (supersedes #55631)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `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`
- **#41389: fix(desktop): extend PATH for dashboard backend so codex is discoverable on macOS**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#44814`, `#41385`
- **#51251: fix(desktop): include user local bin in backend PATH**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`
- **#51648: fix(computer_use): resolve cua-driver in canonical install dirs under thin GUI PATH (#51393)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#69902`

## Fix status

- Merged to main in PR #44814 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-08-17`.
- Subsystem: `agent`
