# NousResearch/hermes-agent · Windows/WSL path format mismatch in terminal CWD handling

## Open now

- Nothing open on GitHub.

These issues and pull requests concern a problem where Windows-native path formats leak into terminal sessions running under WSL or Git Bash, causing 'cd' failures and broken subsequent commands. The work involves detecting and converting Windows paths to MSYS/Unix-compatible formats for terminal tool operations.

## TL;DR

- 10 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 1 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #56384. Verify it addresses your case.
- **Recommended:** read PR #56384 (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: `partially_resolved` · deciding PR(s): `pr-56384`. 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.19`: 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

- #35459: Windows terminal: cd fails with 'No such file or directory' for Windows native paths
- #40137: Terminal wrapper injects Windows paths when running in WSL
- #50594: Windows: session cwd in native format breaks terminal tool
- #50820: \[BUG\] terminal tool CWD wedge: Windows session CWD leaks into WSL bash as invalid path

## Prior & competing PRs

- **#56384: fix(terminal): force Git for Windows bash + convert native CWD for bash cd**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_local_env_windows_msys.py`, `tools/environments/base.py`, `tools/environments/local.py`
  - fix hunks:
  - `tests/tools/test_local_env_windows_msys.py`

    ```diff
    @@ -26,6 +26,7 @@
         LocalEnvironment,
         _msys_to_windows_path,
         _resolve_safe_cwd,
    +    _windows_to_msys_path,
     )
     
     
    ```

    ```diff
    @@ -70,6 +71,35 @@ def test_empty_string(self, monkeypatch):
             assert _msys_to_windows_path("") == ""
     
     
    +# ---------------------------------------------------------------------------
    +# _windows_to_msys_path — reverse translation for bash builtin cd
    +# ---------------------------------------------------------------------------
    +
    +class TestWindowsToMsysPath:
    +    def test_noop_on_non_windows(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", False)
    +        assert _windows_to_msys_path(r"C:\Users\NVIDIA") == r"C:\Users\NVIDIA"
    +
    +    def test_translates_backslash_path(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +        assert _windows_to_msys_path(r"C:\Users\NVIDIA") == "/c/Users/NVIDIA"
    +        assert _windows_to_msys_path(r"D:\Projects\foo bar") == "/d/Projects/foo bar"
    +
    +    def test_translates_forward_slash_native_path(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +        assert _windows_to_msys_path("C:/Users/NVIDIA") == "/c/Users/NVIDIA"
    +
    +    def test_translates_drive_root(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +        assert _windows_to_msys_path(r"C:\\") == "/c/"
    +        assert _windows_to_msys_path("D:/") == "/d/"
    +
    +    def test_does_not_translate_non_drive_path(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +        assert _windows_to_msys_path("/tmp/foo") == "/tmp/foo"
    +        assert _windows_to_msys_path(r"\\server\share") == r"\\server\share"
    +
    +
     # ---------------------------------------------------------------------------
     # _resolve_safe_cwd — Windows fast path
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -196,3 +226,44 @@ def test_valid_msys_marker_normalized_to_native(self, monkeypatch, tmp_path):
                 env._extract_cwd_from_output(result)
     
             assert env.cwd == str(new_dir)
    +
    +
    +# ---------------------------------------------------------------------------
    +# Command wrapping — native Windows cwd must be Git Bash-friendly for cd
    +# ---------------------------------------------------------------------------
    +
    +class TestWrapCommandWindowsNativeCwd:
    +    def test_wrap_command_converts_native_cwd_for_builtin_cd(self, monkeypatch):
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +
    +        with patch.object(
    +            LocalEnvironment, "init_session", autospec=True, return_value=None
    +        ):
    +            env = LocalEnvironment(cwd=r"C:\Users\liush", timeout=10)
    +
    +        env._snapshot_ready = True
    +        wrapped = env._wrap_command("pwd", r"C:\Users\liush")
    +
    +        assert "builtin cd -- /c/Users/liush || exit 126" in wrapped
    +        assert r"builtin cd -- C:\Users\liush || exit 126" not in wrapped
    +
    +    def test_init_session_bootstrap_converts_native_cwd_for_cd(self, monkeypatch):
    +        """The snapshot bootstrap ``cd`` must also use the Git-Bash path form,
    +        not just ``_wrap_command`` — otherwise ``pwd -P`` captures the login
    +        shell's directory instead of ``terminal.cwd`` on Windows."""
    +        monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
    +
    +        captured = {}
    +
    +        def fake_run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):
    +            captured["script"] = cmd_string
    +            raise RuntimeError("stop after capturing bootstrap")
    +
    +        monkeypatch.setattr(LocalEnvironment, "_run_bash", fake_run_bash)
    +
    +        # init_session swallows the exception and falls back; we only need the
    +        # captured bootstrap script to assert the cd target was converted.
    +        LocalEnvironment(cwd=r"C:\Users\liush", timeout=10)
    +
    +        assert "builtin cd -- /c/Users/liush 2>/dev/null || true" in captured["script"]
    +        assert r"C:\Users\liush" not in captured["script"]
    ```
  - `tools/environments/base.py`

    ```diff
    @@ -361,7 +361,12 @@ def init_session(self):
             # Restore configured cwd after login shell profile scripts, which may
             # change the working directory (e.g. bashrc `cd ~`).  Without this,
             # pwd -P captures the profile's directory, not terminal.cwd.
    -        _quoted_cwd = shlex.quote(self.cwd)
    +        # Route through ``_quote_cwd_for_cd`` (not a bare ``shlex.quote``) so
    +        # the Windows subclass override converts a native ``C:\Users\x`` cwd to
    +        # the Git-Bash ``/c/Users/x`` form the bootstrap ``cd`` can resolve.
    +        # Without this the snapshot bootstrap ``cd`` below fails on Windows and
    +        # ``pwd -P`` captures the login shell's directory, not ``terminal.cwd``.
    +        _quoted_cwd = self._quote_cwd_for_cd(self.cwd)
             # Quote the snapshot / cwd-file paths so Git Bash on Windows handles
             # ``C:/Users/...``-shaped paths without glob-splitting the colon or
             # tripping on drive letters.  On POSIX this is a no-op (no colons /
    ```

    ```diff
    @@ -413,7 +418,7 @@ def init_session(self):
                 # Publish atomically only if assembly succeeded; otherwise drop the
                 # partial temp rather than leave it to be sourced or orphaned.
                 f"mv -f {_snap_tmp} {_quoted_snap} || rm -f {_snap_tmp}\n"
    -            f"builtin cd {_quoted_cwd} 2>/dev/null || true\n"
    +            f"builtin cd -- {_quoted_cwd} 2>/dev/null || true\n"
                 f"pwd -P > {_quoted_cwd_file} 2>/dev/null || true\n"
                 f"printf '\\n{self._cwd_marker}%s{self._cwd_marker}\\n' \"$(pwd -P)\"\n"
             )
    ```
  - `tools/environments/local.py`

    ```diff
    @@ -40,6 +40,24 @@ def _msys_to_windows_path(cwd: str) -> str:
         return f"{drive}:{tail or chr(92)}"  # chr(92) = backslash, avoid raw-string escape
     
     
    +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.
    +
    +    No-ops on non-Windows hosts or for paths that aren't drive-qualified
    +    native Windows paths. Returns the input unchanged when no translation
    +    applies.
    +    """
    +    if not _IS_WINDOWS or not cwd:
    +        return cwd
    +    m = re.match(r'^([a-zA-Z]):[\\/]*(.*)$', cwd)
    +    if not m:
    +        return cwd
    +    drive = m.group(1).lower()
    +    tail = (m.group(2) or "").replace('\\', '/').lstrip('/')
    +    return f"/{drive}/{tail}" if tail else f"/{drive}/"
    +
    +
     def _resolve_safe_cwd(cwd: str) -> str:
         """Return ``cwd`` if it exists as a directory, else the nearest existing
         ancestor.  Falls back to ``tempfile.gettempdir()`` only if walking up the
    ```

    ```diff
    @@ -509,10 +527,10 @@ def _find_bash() -> str:
                 if os.path.isfile(candidate):
                     return candidate
     
    -    found = shutil.which("bash")
    -    if found:
    -        return found
    -
    +    # Check known Git for Windows install locations before PATH lookup.
    +    # On machines with both WSL and Git for Windows, shutil.which("bash")
    +    # may return WSL's bash (which doesn't understand Windows paths and
    +    # will fail silently).  Explicit Git-for-Windows paths avoid that.
         for candidate in (
             os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "Git", "bin", "bash.exe"),
             os.path.join(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"), "Git", "bin", "bash.exe"),
    ```

    ```diff
    @@ -521,6 +539,10 @@ def _find_bash() -> str:
             if candidate and os.path.isfile(candidate):
                 return candidate
     
    +    found = shutil.which("bash")
    +    if found:
    +        return found
    +
         raise RuntimeError(
             "Git Bash not found. Hermes Agent requires Git for Windows on Windows.\n"
             "Install it from: https://git-scm.com/download/win\n"
    ```

    ```diff
    @@ -917,6 +939,11 @@ def get_temp_dir(self) -> str:
     
             return "/tmp"
     
    +    @staticmethod
    +    def _quote_cwd_for_cd(cwd: str) -> str:
    +        """Use native paths for Python, but Git Bash-friendly paths for cd."""
    +        return BaseEnvironment._quote_cwd_for_cd(_windows_to_msys_path(cwd))
    +
         def _run_bash(self, cmd_string: str, *, login: bool = False,
                       timeout: int = 120,
                       stdin_data: str | None = None) -> subprocess.Popen:
    ```
- **#13397: fix: Windows stale-cwd causes exit 126 on all subsequent commands**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#50594`, `#56384`, `#17558`
- **#35653: fix(terminal): normalize Windows cwd for Git Bash cd**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `9ed7252a9`, `#56384`
- **#40247: fix(wsl): translate Windows cwd values for terminal tools**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `3c7b9f2e9d00025216e663e024b1ee6f415d959c`, `#40137`
- **#49833: fix(terminal): resolve Windows path format mismatch in LocalEnvironment**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `ad5f3341d`, `5d613a563`, `#56384`, `#47837`
- **#50596: fix(tools): convert Windows native CWD to MSYS format for bash cd**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#56384`

## Fix status

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