# NousResearch/hermes-agent · TUI crashes when cwd contains shadowing Python packages

## Open now

- Nothing open on GitHub.

These issues and fixes address a bug where the TUI or its gateway child process fails to launch when the current working directory contains a Python package (e.g., 'utils', 'proxy', 'ui') that shadows internal modules via sys.path. The fixes guard sys.path in the slash_worker and tui_gateway to prevent local packages from interfering with internal imports.

## TL;DR

- 8 member(s): 0 open issue(s), 2 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 #19885. Verify it addresses your case.
- **Recommended:** read PR #19885 (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-19885,pr-51693`. 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.23`: 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

- #15989: TUI fails to launch when cwd contains a Python package named 'utils/'
- #51286: \[Bug\]: Gateway child crashes (exit code 1) when cwd contains Python package named \`utils\`

## Prior & competing PRs

- **#19885: fix(tui_gateway): guard sys.path against local package shadowing**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tui_gateway/test_entry_sys_path.py`, `tui_gateway/entry.py`
  - fix hunks:
  - `tests/tui_gateway/test_entry_sys_path.py`

    ```diff
    @@ -0,0 +1,101 @@
    +"""Tests for tui_gateway/entry.py sys.path hardening (issue #15989).
    +
    +When the TUI backend is spawned by Node.js, the Python interpreter may have
    +'' or '.' at the front of sys.path, allowing a local utils/ directory in CWD
    +to shadow the installed utils module.  entry.py must sanitize sys.path before
    +any non-stdlib import is resolved.
    +"""
    +
    +import importlib
    +import os
    +import sys
    +from unittest.mock import patch
    +
    +
    +def _reload_entry_with_env(env_overrides: dict) -> None:
    +    """Re-execute entry.py's module-level path setup under a controlled env."""
    +    # We only want to exercise the sys.path fixup block, not the signal/import
    +    # machinery that follows.  We do this by running the fixup code verbatim in
    +    # a fresh copy of sys.path rather than importing the real module (which
    +    # would trigger tui_gateway.server imports requiring heavy mocks).
    +    original_path = sys.path[:]
    +    original_env = {k: os.environ.get(k) for k in env_overrides}
    +    try:
    +        with patch.dict(os.environ, env_overrides, clear=False):
    +            _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "")
    +            if _src_root and _src_root not in sys.path:
    +                sys.path.insert(0, _src_root)
    +            sys.path = [p for p in sys.path if p not in ("", ".")]
    +        return sys.path[:]
    +    finally:
    +        sys.path = original_path
    +        for k, v in original_env.items():
    +            if v is None:
    +                os.environ.pop(k, None)
    +            else:
    +                os.environ[k] = v
    +
    +
    +def test_empty_string_and_dot_removed_from_sys_path():
    +    original = sys.path[:]
    +    try:
    +        sys.path.insert(0, "")
    +        sys.path.insert(0, ".")
    +        assert "" in sys.path
    +        assert "." in sys.path
    +
    +        # Run the entry.py fixup logic directly
    +        sys.path = [p for p in sys.path if p not in ("", ".")]
    +
    +        assert "" not in sys.path
    +        assert "." not in sys.path
    +    finally:
    +        sys.path = original
    +
    +
    +def test_hermes_src_root_inserted_at_front():
    +    original = sys.path[:]
    +    try:
    +        fake_root = "/fake/hermes/src"
    +        with patch.dict(os.environ, {"HERMES_PYTHON_SRC_ROOT": fake_root}):
    +            _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "")
    +            if _src_root and _src_root not in sys.path:
    +                sys.path.insert(0, _src_root)
    +            sys.path = [p for p in sys.path if p not in ("", ".")]
    +
    +        assert sys.path[0] == fake_root
    +    finally:
    +        sys.path = original
    +
    +
    +def test_src_root_not_duplicated_if_already_present():
    +    original = sys.path[:]
    +    try:
    +        fake_root = "/already/present"
    +        sys.path.insert(0, fake_root)
    +        count_before = sys.path.count(fake_root)
    +
    +        with patch.dict(os.environ, {"HERMES_PYTHON_SRC_ROOT": fake_root}):
    +            _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "")
    +            if _src_root and _src_root not in sys.path:
    +                sys.path.insert(0, _src_root)
    +            sys.path = [p for p in sys.path if p not in ("", ".")]
    +
    +        assert sys.path.count(fake_root) == count_before
    +    finally:
    +        sys.path = original
    +
    +
    +def test_no_src_root_env_does_not_crash():
    +    original = sys.path[:]
    +    try:
    +        env = {k: v for k, v in os.environ.items() if k != "HERMES_PYTHON_SRC_ROOT"}
    +        with patch.dict(os.environ, {}, clear=True):
    +            os.environ.update(env)
    +            _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "")
    +            if _src_root and _src_root not in sys.path:
    +                sys.path.insert(0, _src_root)
    +            sys.path = [p for p in sys.path if p not in ("", ".")]
    +        # No exception raised
    +    finally:
    +        sys.path = original
    ```
  - `tui_gateway/entry.py`

    ```diff
    @@ -1,7 +1,18 @@
    -import json
     import os
    -import signal
     import sys
    +
    +# Guard against a local utils/ (or other package) in CWD shadowing installed
    +# hermes modules.  hermes_cli sets HERMES_PYTHON_SRC_ROOT before spawning this
    +# subprocess; inserting it first ensures the installed packages win.
    +_src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "")
    +if _src_root and _src_root not in sys.path:
    +    sys.path.insert(0, _src_root)
    +# Strip '' and '.' — both resolve to CWD at import time and can let a local
    +# directory shadow installed packages.
    +sys.path = [p for p in sys.path if p not in ("", ".")]
    +
    +import json
    +import signal
     import time
     import traceback
     
    ```
- **#51693: fix(tui): stop a cwd package named utils/proxy/ui from crashing the gateway child**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `acp_adapter/entry.py`, `hermes_bootstrap.py`, `tests/test_hermes_bootstrap.py`, `tests/tui_gateway/test_entry_sys_path.py`, `tui_gateway/entry.py`, `ui-tui/src/gatewayClient.ts`
- **#56379: fix(tui): guard slash_worker sys.path against local package shadowing (#51286)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tui_gateway/test_slash_worker_sys_path.py`, `tui_gateway/slash_worker.py`
- **#16141: fix(tui_gateway): guard sys.path against local package shadowing (#15989)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19885`
- **#51304: fix(tui_gateway): prevent cwd packages from shadowing internal modules**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `c39b2b50eeb8c338e147739e6fe3a50e2be86192`, `#51286`, `#51693`
- **#51484: fix(tui): guard slash_worker sys.path against local package shadowing**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#56379`, `#51693`

## Fix status

- Merged to main in PR #19885 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `gateway`
