NousResearch/hermes-agent

Terminal CWD and path normalization bug fixes

Cluster 26783 · environments. Data as of

Nothing open on GitHub. Fix merged in #65475 on 16 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #65475. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #65475. That is separate from the fix event.
Still open
Nothing open on GitHub.

A cluster of fixes addressing working directory handling edge cases: graceful fallbacks when CWD is deleted, normalization of relative and MSYS/Git Bash paths on Windows, and resolving errors when the current directory is already the target.

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.

  • #65475 fix(local): normalize relative initial cwd to an absolute host path

    merged to main

    files: scripts/release.py, tests/tools/test_local_env_relative_cwd.py, tools/environments/local.py

    Show the diff
    • scripts/release.py
      @@ -1990,6 +1990,7 @@
           "[redacted:email]": "dexhunter",  # PR #60339 salvage (skills snapshot manifest speedup)
           "[redacted:email]": "uzaylisak",  # PR #29988 salvage (detect_local_server_type process-lifetime cache)
           "[redacted:email]": "8294",  # PR #50572 salvage (honor config context_length on banner)
      +    "[redacted:email]": "ya-nsh",  # PR #26790 salvage (normalize local terminal relative cwd; #26783)
       }
       
       
    • tests/tools/test_local_env_relative_cwd.py
      @@ -0,0 +1,52 @@
      +"""Regression tests for local terminal initial cwd normalization."""
      +
      +from pathlib import Path
      +
      +from tools.environments.local import LocalEnvironment, _resolve_local_initial_cwd
      +
      +
      +def test_relative_initial_cwd_resolves_from_parent(tmp_path, monkeypatch):
      +    project = tmp_path / "hermes-agent"
      +    project.mkdir()
      +    monkeypatch.chdir(tmp_path)
      +
      +    assert _resolve_local_initial_cwd("hermes-agent") == str(project)
      +
      +
      +def test_relative_initial_cwd_matching_current_dir_uses_current_dir(tmp_path, monkeypatch):
      +    project = tmp_path / "hermes-agent"
      +    project.mkdir()
      +    monkeypatch.chdir(project)
      +
      +    assert _resolve_local_initial_cwd("hermes-agent") == str(project)
      +
      +
      +def test_local_environment_does_not_cd_into_nested_matching_relative_cwd(tmp_path, monkeypatch):
      +    project = tmp_path / "hermes-agent"
      +    project.mkdir()
      +    monkeypatch.chdir(project)
      +
      +    env = LocalEnvironment(cwd="hermes-agent", timeout=5)
      +    try:
      +        result = env.execute("pwd", timeout=5)
      +    finally:
      +        env.cleanup()
      +
      +    assert result["returncode"] == 0
      +    assert result["output"].strip() == str(project)
      +    assert "cd: hermes-agent" not in result["output"]
      +
      +
      +def test_local_environment_keeps_existing_relative_child_cwd(tmp_path, monkeypatch):
      +    project = tmp_path / "hermes-agent"
      +    project.mkdir()
      +    monkeypatch.chdir(tmp_path)
      +
      +    env = LocalEnvironment(cwd="hermes-agent", timeout=5)
      +    try:
      +        result = env.execute("pwd", timeout=5)
      +    finally:
      +        env.cleanup()
      +
      +    assert result["returncode"] == 0
      +    assert result["output"].strip() == str(project)
    • tools/environments/local.py
      @@ -48,6 +48,46 @@ def _msys_to_windows_path(cwd: str) -> str:
           return f"{drive}:{tail or chr(92)}"  # chr(92) = backslash, avoid raw-string escape
       
       
      +def _resolve_local_initial_cwd(cwd: str) -> str:
      +    """Resolve the local backend's initial cwd to an absolute host path.
      +
      +    ``TERMINAL_CWD`` can be populated from config.yaml before the terminal
      +    backend is created.  If that value is relative and happens to match the
      +    directory Hermes was already launched from (for example ``hermes-agent``
      +    while the process cwd is ``~/.hermes/hermes-agent``), passing it through
      +    unchanged makes the wrapper run ``cd hermes-agent`` *inside* the project
      +    and fail with a confusing nested-path error.  Anchor relative local cwd
      +    values once, up front, so both ``subprocess.Popen(cwd=...)`` and the
      +    in-shell ``cd`` use the same absolute directory.
      +    """
      +    expanded = os.path.expanduser(cwd) if cwd else os.getcwd()
      +    if _IS_WINDOWS:
      +        expanded = _msys_to_windows_path(expanded)
      +        # Use the Windows-aware check explicitly: when _IS_WINDOWS is
      +        # patched in tests on a POSIX host, os.path.isabs would reject
      +        # ``C:\Users\x`` and mangle it through the relative branch.
      +        import ntpath
      +        if ntpath.isabs(expanded):
      +            return expanded
      +    if os.path.isabs(expanded):
      +        return expanded
      +
      +    candidate = os.path.abspath(expanded)
      +    current = os.getcwd()
      +
      +    # Common recovery for config values like ``hermes-agent`` when Hermes was
      +    # launched from that directory already.  ``os.path.abspath`` would point at
      +    # a nonexistent nested ``./hermes-agent``; use the current directory instead.
      +    if not os.path.isdir(candidate):
      +        wanted_parts = Path(expanded).parts
      +        current_parts = Path(current).parts
      +        if wanted_parts and len(wanted_parts) <= len(current_parts):
      +            if current_parts[-len(wanted_parts):] == wanted_parts:
      +                return current
      +
      +    return candidate
      +
      +
       def _windows_to_msys_path(cwd: str) -> str:
           """Translate a native Windows path (``C:\\Users\\x``) to Git Bash /
           MSYS form (``/c/Users/x``) so ``builtin cd`` resolves it reliably.
      @@ -1102,9 +1142,8 @@ class LocalEnvironment(BaseEnvironment):
           """
       
           def __init__(self, cwd: str = "", timeout: int = 60, env: dict = None):
      -        if cwd:
      -            cwd = os.path.expanduser(cwd)
      -        super().__init__(cwd=cwd or os.getcwd(), timeout=timeout, env=env)
      +        cwd = _resolve_local_initial_cwd(cwd)
      +        super().__init__(cwd=cwd, timeout=timeout, env=env)
               self.init_session()
       
           def get_temp_dir(self) -> str:

1 PR closed without merging

From the close comments: merged via another change 1

  • #26790 fix: normalize local terminal relative cwd · merged via another change → 5458c76, #65475
Closed issues · 1
  • Closed#26783terminal tool fails with `cd: hermes-agent: No such file or directory` when CWD is already the target directory
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: 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
environments
Merged PRs
#65475
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
16 Jul 2026

For your agent

The same facts, typed.