TUI crashes when cwd contains shadowing Python packages

NousResearch/hermes-agent · cluster 15989 · gateway

Data as of

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

The short answer

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

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.

  • #19885 fix(tui_gateway): guard sys.path against local package shadowing

    merged to main

    files: tests/tui_gateway/test_entry_sys_path.py, tui_gateway/entry.py

    Show the diff
    • tests/tui_gateway/test_entry_sys_path.py
      @@ -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
      @@ -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

    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

    files: tests/tui_gateway/test_slash_worker_sys_path.py, tui_gateway/slash_worker.py

3 PRs closed without merging

From the close comments: merged via another change 2 · done elsewhere 1

  • #16141 fix(tui_gateway): guard sys.path against local package shadowing (#15989) · merged via another change → #19885
  • #51304 fix(tui_gateway): prevent cwd packages from shadowing internal modules · done elsewhere → c39b2b5, #51286, #51693
  • #51484 fix(tui): guard slash_worker sys.path against local package shadowing · merged via another change → #56379, #51693
Closed issues · 2
  • Closed#15989TUI fails to launch when cwd contains a Python package named 'utils/'
  • Closed#51286[Bug]: Gateway child crashes (exit code 1) when cwd contains Python package named `utils`
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.23: 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
gateway
Merged PRs
#19885, #51693, #56379
Closed issues
2
Attempts
3 PRs closed without merging
Fix link checked
1 Jul 2026

For your agent

The same facts, typed.