# NousResearch/hermes-agent · Terminal CWD and path normalization bug fixes

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #65475. Verify it addresses your case.
- **Recommended:** read PR #65475 (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-65475`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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

- #26783: terminal tool fails with \`cd: hermes-agent: No such file or directory\` when CWD is already the target directory

## Prior & competing PRs

- **#65475: fix(local): normalize relative initial cwd to an absolute host path**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `scripts/release.py`, `tests/tools/test_local_env_relative_cwd.py`, `tools/environments/local.py`
  - fix hunks:
  - `scripts/release.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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.
    ```

    ```diff
    @@ -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:
    ```
- **#26790: fix: normalize local terminal relative cwd**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `5458c7656`, `#65475`

## Fix status

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