macOS desktop app missing PATH entries for tools

NousResearch/hermes-agent · cluster 41385 · agent

Data as of

Nothing open on GitHub. Fix merged in #44814. Check it covers your case.

The short answer

0open issues
0open PRs
9items grouped
Known?
Yes. 9 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #44814. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #69696, #69902. That is separate from the fix event.
Still open
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.

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.

  • #44814 fix(desktop): complete backend PATH for Homebrew Codex

    merged to main

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

    Show the diff
    • apps/desktop/electron/backend-env.cjs
      @@ -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
      @@ -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
      @@ -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 {
      @@ -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
      @@ -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
      @@ -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

    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

    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

3 PRs closed without merging

From the close comments: superseded by another PR 2 · won't fix / by design 1

  • #41389 fix(desktop): extend PATH for dashboard backend so codex is discoverable on macOS · superseded by another PR → #44814, #41385
  • #51251 fix(desktop): include user local bin in backend PATH · won't fix / by design
  • #51648 fix(computer_use): resolve cua-driver in canonical install dirs under thin GUI PATH (#51393) · superseded by another PR → #69902
Closed issues · 3
  • Closed#41385[Bug]: /codex-runtime on fails on macOS desktop — dashboard inherits minimal PATH without ~/.hermes/node/bin
  • Closed#51249[Bug]: Hermes Desktop backend PATH misses ~/.local/bin
  • Closed#51393computer_use tool silently unavailable in macOS desktop app (works in CLI) — which() gate misses ~/.local/bin under launchd PATH
How this cluster was grouped
  • Link evidence (partly structural links): 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 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

Subsystem
agent
Merged PRs
#44814, #69696, #69902
Closed issues
3
Attempts
3 PRs closed without merging
Fix link checked
17 Aug 2026

For your agent

The same facts, typed.