Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,14 +568,21 @@ def load_cli_config() -> Dict[str, Any]:

# CWD resolution for CLI/TUI. The gateway has its own config bridge in
# gateway/run.py but may lazily import cli.py (triggering this code).
# Local backend: always os.getcwd(). Use `cd /dir && hermes` to control it.
# Local backend: always the live CWD. Use `cd /dir && hermes` to control it.
# Non-local with placeholder: pop so terminal_tool uses its per-backend default.
# Non-local with explicit path: keep as-is.
_CWD_PLACEHOLDERS = (".", "auto", "cwd")
effective_backend = terminal_config.get("env_type", "local")

if effective_backend == "local":
terminal_config["cwd"] = os.getcwd()
# _safe_getcwd() tolerates a deleted launch directory: os.getcwd()
# raises FileNotFoundError when the CWD is removed out from under the
# process, which would otherwise crash config loading. It shares
# terminal_tool's fallback (TERMINAL_CWD, then home) so the resolved
# cwd stays consistent with the backend that consumes it.
from tools.terminal_tool import _safe_getcwd

terminal_config["cwd"] = _safe_getcwd()
defaults["terminal"]["cwd"] = terminal_config["cwd"]
elif terminal_config.get("cwd") in _CWD_PLACEHOLDERS:
terminal_config.pop("cwd", None)
Expand Down
50 changes: 50 additions & 0 deletions tests/cli/test_cwd_env_respect.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Non-local with explicit path: keep as-is.
"""

import os


_CWD_PLACEHOLDERS = (".", "auto", "cwd")

Expand Down Expand Up @@ -97,3 +99,51 @@ def test_cli_overwrites_stale_env(self):
d = {"terminal": {"cwd": "/home/user"}}
result = _resolve_cwd(tc, d, env)
assert result == "/fake/getcwd"


class TestDeletedCwd:
"""A deleted launch directory must not crash load_cli_config().

os.getcwd() raises FileNotFoundError when the process's working directory
is removed out from under it (e.g. a scratch workspace cleaned up
mid-session). The local-backend cwd bridge must fall back safely instead
of propagating the crash through CLI/TUI (and gateway lazy-import) startup.
"""

def test_load_cli_config_falls_back_to_terminal_cwd(self, tmp_path, monkeypatch):
import cli

hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setattr(cli, "_hermes_home", hermes_home)

def _boom():
raise FileNotFoundError("[Errno 2] No such file or directory")

monkeypatch.setattr(os, "getcwd", _boom)
monkeypatch.setenv("TERMINAL_CWD", "/srv/fallback")

cfg = cli.load_cli_config()

assert cfg["terminal"]["cwd"] == "/srv/fallback"
assert os.environ["TERMINAL_CWD"] == "/srv/fallback"

def test_load_cli_config_falls_back_to_home(self, tmp_path, monkeypatch):
import cli

hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setattr(cli, "_hermes_home", hermes_home)

def _boom():
raise FileNotFoundError()

monkeypatch.setattr(os, "getcwd", _boom)
monkeypatch.delenv("TERMINAL_CWD", raising=False)
monkeypatch.setattr(os.path, "expanduser", lambda p: "/home/me")

cfg = cli.load_cli_config()

assert cfg["terminal"]["cwd"] == "/home/me"
Loading