# NousResearch/hermes-agent · Session-scoped working directory tracking and fixes

## Open now

- 5 issues, 4 PRs open on GitHub.
- #10309: Feature: session-scoped repo pinning to preserve repository identity across compression/resume
- #29531: Per-session working directory for gateway (OpenAI-compatible API) sessions
- #35745: \[Bug\]: Runtime footer displays stale TERMINAL_CWD instead of the agent's live working directory
- #37277: \[Feature\]: Add session-scoped workspace binding for gateway chats
- #50195: feat: Add ability to switch working directory during session
- PR #35767: fix(gateway): show live cwd in runtime footer
- PR #104615: fix(gateway): persist per-session workspaces
- PR #105478: feat(api-server): bind per-run workspace cwd on /v1/runs
- PR #105991: feat(tui): add /cd slash command and scope path completion to session cwd

These issues and PRs center on per-session working directory management: fixing stale CWD values in CLI, TUI, runtime footer, and code execution, and adding features to set or switch the working directory per session across CLI, desktop, and gateway contexts. A small number of members touch unrelated skills/asset lookup behavior.

## TL;DR

- 23 member(s): 5 open issue(s), 3 closed issue(s), 4 open PR(s), 2 merged PR(s), 9 PR(s) closed without merging.
- A fix was merged to main in PR #65213. Verify it addresses your case.
- **Recommended:** read PR #65213 (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-65487`. 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.07`: 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.

## Open related issues

- #10309: Feature: session-scoped repo pinning to preserve repository identity across compression/resume
- #29531: Per-session working directory for gateway (OpenAI-compatible API) sessions
- #35745: \[Bug\]: Runtime footer displays stale TERMINAL_CWD instead of the agent's live working directory
- #37277: \[Feature\]: Add session-scoped workspace binding for gateway chats
- #50195: feat: Add ability to switch working directory during session

## Closed / duplicate issues

- #42525: feat(desktop): allow changing workspace/directory from Hermes Desktop UI
- #49637: \[Bug\]: TUI reuses stale HERMES_CWD instead of current launch cwd
- #56047: Bug: execute_code sandbox ignores the per-session cwd override, diverges from write_file/terminal

## Prior & competing PRs

- **#35768: fix(skills): ignore nested assets in skill lookup**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#49662: fix(cli): use live launch cwd for HERMES_CWD instead of stale env**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#49658`, `#49658`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#56406: fix(tools): honor per-session cwd override in execute_code project mode**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#35513`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#65213: refactor(terminal): per-session cwd records replace shared-env cwd tracking**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/agent/test_file_safety_credentials.py`, `tests/tools/test_container_cwd_sanitize.py`, `tests/tools/test_file_staleness.py`, `tests/tools/test_file_tools.py`, `tests/tools/test_file_tools_cwd_resolution.py`, `tests/tools/test_file_tools_tilde_profile.py`, `tests/tools/test_resolve_path.py`, `tests/tools/test_session_cwd_store.py`, `tests/tools/test_terminal_task_cwd.py`, `tools/delegate_tool.py`, `tools/file_tools.py`, `tools/terminal_tool.py`
  - fix hunks:
  - `tests/agent/test_file_safety_credentials.py`

    ```diff
    @@ -144,6 +144,7 @@ def test_read_file_tool_blocks_relative_path_under_terminal_cwd(
         import json
     
         import tools.file_tools as ft
    +    import tools.terminal_tool as terminal_tool
     
         _create(fake_home, "auth.json")
         # Force the file_tools resolver to anchor relative paths at HERMES_HOME
    ```

    ```diff
    @@ -151,7 +152,7 @@ def test_read_file_tool_blocks_relative_path_under_terminal_cwd(
         monkeypatch.setenv("TERMINAL_CWD", str(fake_home))
         monkeypatch.chdir(tmp_path)
         monkeypatch.setattr(
    -        ft, "_get_live_tracking_cwd", lambda task_id="default": None
    +        terminal_tool, "_session_cwd", {}
         )
     
         out = json.loads(ft.read_file_tool("auth.json"))
    ```

    ```diff
    @@ -166,6 +167,7 @@ def test_read_file_tool_blocks_nested_google_oauth_path(
         import json
     
         import tools.file_tools as ft
    +    import tools.terminal_tool as terminal_tool
     
         oauth = _create(fake_home, Path("auth") / "google_oauth.json")
         oauth.write_text(
    ```

    ```diff
    @@ -180,7 +182,7 @@ def test_read_file_tool_blocks_nested_google_oauth_path(
         )
         monkeypatch.chdir(tmp_path)
         monkeypatch.setattr(
    -        ft, "_get_live_tracking_cwd", lambda task_id="default": None
    +        terminal_tool, "_session_cwd", {}
         )
     
         out = json.loads(ft.read_file_tool(str(oauth), task_id="google-oauth-test"))
    ```

    ```diff
    @@ -195,6 +197,7 @@ def test_search_tool_blocks_direct_auth_json_path(fake_home, monkeypatch):
         import json
     
         import tools.file_tools as ft
    +    import tools.terminal_tool as terminal_tool
     
         auth = _create(fake_home, "auth.json")
         auth.write_text("SEARCH_DIRECT_AUTH_SECRET", encoding="utf-8")
    ```

    ```diff
    @@ -223,6 +226,7 @@ def test_search_tool_filters_credential_results(fake_home, tmp_path, monkeypatch
     
         from tools.file_operations import SearchMatch, SearchResult
         import tools.file_tools as ft
    +    import tools.terminal_tool as terminal_tool
     
         auth = _create(fake_home, "auth.json")
         token = [redacted:secret-assignment] Path("mcp-tokens") / "provider.json")
    ```

    ```diff
    @@ -256,7 +260,7 @@ def search(self, **kwargs):
         monkeypatch.chdir(tmp_path)
         monkeypatch.setattr(ft, "_get_file_ops", lambda task_id="default": FakeFileOps())
         monkeypatch.setattr(
    -        ft, "_get_live_tracking_cwd", lambda task_id="default": None
    +        terminal_tool, "_session_cwd", {}
         )
     
         search_response = ft.search_tool(
    ```
  - `tests/tools/test_container_cwd_sanitize.py`

    ```diff
    @@ -211,7 +211,7 @@ def fake_create_environment(env_type, image, cwd, timeout, **kwargs):
             monkeypatch.setattr(tt, "_active_environments", {})
             monkeypatch.setattr(tt, "_last_activity", {})
             monkeypatch.setattr(ft, "_file_ops_cache", {})
    -        monkeypatch.setattr(ft, "_last_known_cwd", {})
    +        monkeypatch.setattr(tt, "_session_cwd", {})
     
             task_id = "sess-fileops-host-cwd"
             tt.register_task_env_overrides(task_id, {"cwd": override_cwd})
    ```
  - `tests/tools/test_file_staleness.py`

    ```diff
    @@ -150,8 +150,8 @@ def test_different_task_isolated(self, mock_ops):
             self.assertNotIn("_warning", result)
     
         @patch("tools.file_tools._get_file_ops")
    -    def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):
    -        """Relative-path stale tracking must follow the live terminal cwd."""
    +    def test_relative_path_uses_recorded_session_cwd_for_staleness_tracking(self, mock_ops):
    +        """Relative-path stale tracking must follow the session's recorded cwd."""
             start_dir = os.path.join(self._tmpdir, "start")
             live_dir = os.path.join(self._tmpdir, "worktree")
             os.makedirs(start_dir, exist_ok=True)
    ```

    ```diff
    @@ -165,15 +165,12 @@ def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):
                 f.write("live copy\n")
     
             fake_ops = _make_fake_ops("live copy\n", 10)
    -        fake_ops.env = SimpleNamespace(cwd=live_dir)
    -        fake_ops.cwd = start_dir
             mock_ops.return_value = fake_ops
     
    -        from tools import file_tools
    +        from tools import terminal_tool
     
    -        with file_tools._file_ops_lock:
    -            previous = file_tools._file_ops_cache.get("live_task")
    -            file_tools._file_ops_cache["live_task"] = fake_ops
    +        # The session cd'd into the worktree (recorded by the completed command).
    +        terminal_tool.record_session_cwd("live_task", live_dir)
     
             try:
                 with patch.dict(os.environ, {"TERMINAL_CWD": start_dir}, clear=False):
    ```

    ```diff
    @@ -187,11 +184,7 @@ def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):
                         write_file_tool("shared.txt", "replacement", task_id="live_task")
                     )
             finally:
    -            with file_tools._file_ops_lock:
    -                if previous is None:
    -                    file_tools._file_ops_cache.pop("live_task", None)
    -                else:
    -                    file_tools._file_ops_cache["live_task"] = previous
    +            terminal_tool.clear_session_cwd("live_task")
     
             self.assertIn("_warning", result)
             self.assertIn("modified since you last read", result["_warning"])
    ```
  - `tests/tools/test_file_tools.py`

    ```diff
    @@ -677,13 +677,16 @@ def test_no_anyof_required_stays_mode_only(self):
     
     
     # ---------------------------------------------------------------------------
    -# _last_known_cwd tests (#26211: silent file creation failure in long conversations)
    +# Session-cwd persistence across env recreation (#26211: silent file creation
    +# failure in long conversations). The durable anchor is the per-session cwd
    +# record in terminal_tool; env cleanup cannot lose it because it never lived
    +# on the env.
     # ---------------------------------------------------------------------------
     
    -class TestLastKnownCwd:
    +class TestSessionCwdSurvivesEnvRecreation:
         """
         When the terminal environment is cleaned up and re-created during a long
    -    conversation, _last_known_cwd preserves the old environment's CWD so
    +    conversation, the session's cwd record preserves the working directory so
         subsequent file writes with relative paths land in the right directory.
     
         Regression guard for issue #26211.
    ```

    ```diff
    @@ -693,12 +696,12 @@ class TestLastKnownCwd:
         @patch("tools.file_tools._file_ops_cache", new_callable=dict)
         @patch("tools.terminal_tool._get_env_config")
         @patch("tools.terminal_tool._create_environment")
    -    def test_last_known_cwd_preserved_across_env_recreation(
    +    def test_recorded_cwd_used_for_recreated_env(
             self, mock_create_env, mock_config, mock_cache, mock_active
         ):
    -        from tools.file_tools import _get_file_ops, _last_known_cwd
    +        import tools.terminal_tool as tt
    +        from tools.file_tools import _get_file_ops
     
    -        # Setup: create a mock env with a known CWD
             mock_env = MagicMock()
             mock_env.cwd = "/Users/user/project"
             mock_create_env.return_value = mock_env
    ```

    ```diff
    @@ -709,42 +712,35 @@ def test_last_known_cwd_preserved_across_env_recreation(
             }
     
             task_id = "default"
    +        # The session's record holds the directory (written by the last
    +        # completed terminal command before the env was cleaned up).
    +        tt.record_session_cwd(task_id, "/Users/user/project")
    +        try:
    +            _get_file_ops(task_id)
    +
    +            create_call = mock_create_env.call_args
    +            assert create_call is not None, "_create_environment was not called"
    +            kwargs = create_call.kwargs if create_call.kwargs else {}
    +            cwd_passed = kwargs.get("cwd", None)
    +            if cwd_passed is None:
    +                args = create_call.args if create_call.args else []
    +                if len(args) >= 3:
    +                    cwd_passed = args[2]
    +
    +            assert cwd_passed == "/Users/user/project", \
    +                f"Expected cwd='/Users/user/project', got {cwd_passed!r}"
    +        finally:
    +            tt.clear_session_cwd(task_id)
     
    -        # Preset _last_known_cwd to simulate a previous env's CWD
    -        _last_known_cwd[task_id] = "/Users/user/project"
    -
    -        # Call _get_file_ops - should use _last_known_cwd for the new env
    -        result = _get_file_ops(task_id)
    -
    -        # Verify the env was created with the saved CWD, not the default
    -        create_call = mock_create_env.call_args
    -        assert create_call is not None, "_create_environment was not called"
    -        
    -        # Find cwd in the kwargs
    -        kwargs = create_call.kwargs if create_call.kwargs else {}
    -        # cwd is passed as positional or keyword
    -        cwd_passed = kwargs.get("cwd", None)
    -        if cwd_passed is None:
    -            # Try positional args
    -            args = create_call.args if create_call.args else []
    -            # Position: (env_type, image, cwd, timeout, ...)
    -            if len(args) >= 3:
    -                cwd_passed = args[2]
    -        
    -        assert cwd_passed == "/Users/user/project", \
    -            f"Expected cwd='/Users/user/project', got {cwd_passed!r}"
    -        
    -        # Cleanup
    -        _last_known_cwd.pop(task_id, None)
    -        
         @patch("tools.terminal_tool._active_environments", new_callable=dict)
         @patch("tools.file_tools._file_ops_cache", new_callable=dict)
         @patch("tools.terminal_tool._get_env_config")
         @patch("tools.terminal_tool._create_environment")
    -    def test_last_known_cwd_falls_back_to_config_default_when_not_set(
    +    def test_falls_back_to_config_default_when_no_record(
             self, mock_create_env, mock_config, mock_cache, mock_active
         ):
    -        from tools.file_tools import _get_file_ops, _last_known_cwd
    +        import tools.terminal_tool as tt
    +        from tools.file_tools import _get_file_ops
     
             mock_env = MagicMock()
             mock_env.cwd = "/default/path"
    ```

    ```diff
    @@ -755,85 +751,44 @@ def test_last_known_cwd_falls_back_to_config_default_when_not_set(
                 "timeout": 30,
             }
     
    -        # _get_file_ops resolves to "default"
             task_id = "default"
    -        
    -        # Ensure _last_known_cwd is empty for this task
    -        _last_known_cwd.pop(task_id, None)
    +        tt.clear_session_cwd(task_id)
    +
    +        _get_file_ops(task_id)
     
    -        result = _get_file_ops(task_id)
    -        
             create_call = mock_create_env.call_args
             assert create_call is not None, "_create_environment was not called"
    -        
             kwargs = create_call.kwargs if create_call.kwargs else {}
             cwd_passed = kwargs.get("cwd", None)
             if cwd_passed is None:
                 args = create_call.args if create_call.args else []
                 if len(args) >= 3:
                     cwd_passed = args[2]
    -        
    -        # Should fall back to config default
    +
             assert cwd_passed == "/config/default/path", \
                 f"Expected cwd='/config/default/path', got {cwd_passed!r}"
     
    -    @patch("tools.terminal_tool._active_environments", new_callable=dict)
    -    @patch("tools.file_tools._file_ops_cache", new_callable=dict)
    -    def test_live_cwd_read_mirrors_into_last_known_cwd(self, mock_cache, mock_active):
    -        """Belt-and-suspenders (#26211): every successful live-cwd read records
    -        the cwd in _last_known_cwd, so the durable anchor doesn't depend on the
    -        cleanup-detection branch of _get_file_ops firing."""
    -        from tools.file_tools import _get_live_tracking_cwd, _last_known_cwd
    -
    -        task_id = "default"
    -        _last_known_cwd.pop(task_id, None)
    -
    -        cached = MagicMock()
    -        cached.env = MagicMock()
    -        cached.env.cwd = "/Users/user/project"
    -        cached.env.cwd_owner = "default"
    -        mock_cache[task_id] = cached
    -
    -        live = _get_live_tracking_cwd(task_id)
    -
    -        assert live == "/Users/user/project"
    -        # The read mirrored the live cwd into the durable registry.
    -        assert _last_known_cwd.get(task_id) == "/Users/user/project"
    -        _last_known_cwd.pop(task_id, None)
    -
         @patch("tools.terminal_tool._active_environments", new_callable=dict)
         @patch("tools.file_tools._file_ops_cache", new_callable=dict)
         @patch("tools.terminal_tool._get_env_config")
         @patch("tools.terminal_tool._create_environment")
    -    def test_mirrored_cwd_survives_when_cache_already_cleared(
    +    def test_stale_cache_cwd_rescued_into_record_on_cleanup_detection(
             self, mock_create_env, mock_config, mock_cache, mock_active
         ):
    -        """The original save-old-cwd path only fires when _file_ops_cache still
    -        holds the stale entry. If the cleanup thread popped BOTH dicts first,
    -        _get_file_ops sees cached=None and never saves — but the proactive
    -        mirror from an earlier live read already populated _last_known_cwd, so
    -        the rebuilt env still restores the user's directory."""
    -        from tools.file_tools import (
    -            _get_file_ops, _get_live_tracking_cwd, _last_known_cwd,
    -        )
    +        """If the env died but the file-ops cache entry survived, its cwd is
    +        rescued into the session record before the cache entry is dropped —
    +        the recreated env starts where the user left off."""
    +        import tools.terminal_tool as tt
    +        from tools.file_tools import _get_file_ops
     
             task_id = "default"
    -        _last_known_cwd.pop(task_id, None)
    +        tt.clear_session_cwd(task_id)
     
    -        # 1) Env is alive and the agent has cd'd into the project. A live read
    -        #    (happens on every relative-path resolution) mirrors the cwd.
    +        # Stale cache entry: env was cleaned up, cache still holds the old cwd.
             cached = MagicMock()
    -        cached.env = MagicMock()
    -        cached.env.cwd = "/Users/user/project"
    -        cached.env.cwd_owner = "default"
    +        cached.env = None
    +        cached.cwd = "/Users/user/project"
             mock_cache[task_id] = cached
    -        assert _get_live_tracking_cwd(task_id) == "/Users/user/project"
    -        assert _last_known_cwd.get(task_id) == "/Users/user/project"
    -
    -        # 2) Cleanup thread kills the env AND clears the cache before the next
    -        #    file write — so _get_file_ops' save-old-cwd branch never runs.
    -        mock_cache.pop(task_id, None)
    -        mock_active.clear()
     
             mock_env = MagicMock()
             mock_env.cwd = "/Users/user/project"
    ```

    ```diff
    @@ -844,21 +799,23 @@ def test_mirrored_cwd_survives_when_cache_already_cleared(
                 "timeout": 30,
             }
     
    -        _get_file_ops(task_id)
    +        try:
    +            _get_file_ops(task_id)
     
    -        create_call = mock_create_env.call_args
    -        assert create_call is not None, "_create_environment was not called"
    -        kwargs = create_call.kwargs if create_call.kwargs else {}
    -        cwd_passed = kwargs.get("cwd", None)
    -        if cwd_passed is None:
    -            args = create_call.args if create_call.args else []
    -            if len(args) >= 3:
    -                cwd_passed = args[2]
    +            create_call = mock_create_env.call_args
    +            assert create_call is not None, "_create_environment was not called"
    +            kwargs = create_call.kwargs if create_call.kwargs else {}
    +            cwd_passed = kwargs.get("cwd", None)
    +            if cwd_passed is None:
    +                args = create_call.args if create_call.args else []
    +                if len(args) >= 3:
    +                    cwd_passed = args[2]
     
    -        # Rebuilt env restored the mirrored cwd, NOT the config default.
    -        assert cwd_passed == "/Users/user/project", \
    -            f"Expected restored cwd='/Users/user/project', got {cwd_passed!r}"
    -        _last_known_cwd.pop(task_id, None)
    +            # Rebuilt env restored the rescued cwd, NOT the config default.
    +            assert cwd_passed == "/Users/user/project", \
    +                f"Expected restored cwd='/Users/user/project', got {cwd_passed!r}"
    +        finally:
    +            tt.clear_session_cwd(task_id)
     
     
     class TestSilentFileMisplacementE2E:
    ```

    ```diff
    @@ -868,8 +825,8 @@ class TestSilentFileMisplacementE2E:
         agent cd's into a project, the cleanup thread kills the env, and a later
         relative-path write must land in the project dir (not the config default).
         Mocks miss this because resolution (_resolve_path_for_task) runs BEFORE
    -    _get_file_ops rebuilds the env — only the durable _last_known_cwd fallback
    -    in _authoritative_workspace_root makes the resolved path correct.
    +    _get_file_ops rebuilds the env — only the durable session-cwd record
    +    makes the resolved path correct.
         """
     
         def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monkeypatch):
    ```

    ```diff
    @@ -889,13 +846,13 @@ def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monk
             )
     
             task_id = "default"
    -        ft._last_known_cwd.pop(task_id, None)
    +        tt.clear_session_cwd(task_id)
     
    -        # 1) Env alive; agent has cd'd into the project. A relative write
    -        #    while alive mirrors the live cwd into the durable registry.
    +        # 1) Env alive; agent has cd'd into the project (the completed command
    +        #    recorded the session cwd — simulate that write here).
             fo = ft._get_file_ops(task_id)
             fo.env.cwd = str(project)
    -        fo.env.cwd_owner = "default"
    +        tt.record_session_cwd(task_id, str(project))
             ft.write_file_tool("alive.txt", "1\n", task_id)
             assert (project / "alive.txt").exists()
     
    ```

    ```diff
    @@ -913,4 +870,4 @@ def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monk
             assert not (config_default / "report.txt").exists(), \
                 "file silently misplaced into config default (the #26211 bug)"
     
    -        ft._last_known_cwd.pop(task_id, None)
    +        tt.clear_session_cwd(task_id)
    ```
  - `tests/tools/test_file_tools_cwd_resolution.py`

    ```diff
    @@ -36,8 +36,8 @@ def _isolated_cwd(tmp_path, monkeypatch):
         # Process cwd = decoy, analogous to "main repo" while the terminal is in
         # the worktree.
         monkeypatch.chdir(decoy)
    -    # No live-terminal-cwd tracking recorded yet (fresh-session condition).
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    # No session cwd recorded yet (fresh-session condition).
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         return workspace, decoy
     
     
    ```

    ```diff
    @@ -72,7 +72,7 @@ def test_live_tracking_cwd_wins_over_relative_terminal_cwd(_isolated_cwd, monkey
         """
         workspace, decoy = _isolated_cwd
         monkeypatch.setenv("TERMINAL_CWD", ".")
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    terminal_tool.record_session_cwd("default", str(workspace))
     
         resolved = ft._resolve_path_for_task("target.py", task_id="default")
     
    ```

    ```diff
    @@ -137,7 +137,7 @@ def test_container_relative_path_keeps_container_cwd_symlink(tmp_path, monkeypat
         container_mount.symlink_to(host_project, target_is_directory=True)
         monkeypatch.setattr(terminal_tool, "_get_env_config", lambda: {"env_type": "docker"})
         monkeypatch.setattr(terminal_tool, "_active_environments", {})
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(container_mount))
    +    terminal_tool.record_session_cwd("default", str(container_mount))
     
         resolved = ft._resolve_path_for_task("oilsands-sim/README.md", task_id="default")
     
    ```

    ```diff
    @@ -185,9 +185,9 @@ def test_warning_fires_when_relative_path_escapes_workspace(_isolated_cwd, monke
         """Relative path resolving outside the live workspace must warn."""
         workspace, decoy = _isolated_cwd
         # Live cwd = workspace, but the relative path resolves to decoy (process cwd)
    -    # because TERMINAL_CWD is the poison '.'.  Simulate by pointing live tracking
    -    # at workspace while the resolved path is under decoy.
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    # because TERMINAL_CWD is the poison '.'.  Simulate by recording workspace
    +    # as the session cwd while the resolved path is under decoy.
    +    terminal_tool.record_session_cwd("default", str(workspace))
         resolved_in_decoy = decoy / "target.py"
     
         warn = ft._path_resolution_warning("target.py", resolved_in_decoy, task_id="default")
    ```

    ```diff
    @@ -200,7 +200,7 @@ def test_warning_fires_when_relative_path_escapes_workspace(_isolated_cwd, monke
     
     def test_no_warning_when_relative_path_inside_workspace(_isolated_cwd, monkeypatch):
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    terminal_tool.record_session_cwd("default", str(workspace))
         resolved_in_workspace = workspace / "target.py"
     
         warn = ft._path_resolution_warning("target.py", resolved_in_workspace, task_id="default")
    ```

    ```diff
    @@ -210,7 +210,7 @@ def test_no_warning_when_relative_path_inside_workspace(_isolated_cwd, monkeypat
     
     def test_no_warning_for_absolute_input(_isolated_cwd, monkeypatch):
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    terminal_tool.record_session_cwd("default", str(workspace))
     
         warn = ft._path_resolution_warning(str(decoy / "target.py"), decoy / "target.py", task_id="default")
     
    ```

    ```diff
    @@ -219,7 +219,7 @@ def test_no_warning_for_absolute_input(_isolated_cwd, monkeypatch):
     
     def test_no_warning_when_no_live_cwd(_isolated_cwd, monkeypatch):
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.delenv("TERMINAL_CWD", raising=False)
     
         warn = ft._path_resolution_warning("target.py", decoy / "target.py", task_id="default")
    ```

    ```diff
    @@ -244,7 +244,7 @@ def test_sentinel_terminal_cwd_is_treated_as_unset(_isolated_cwd, monkeypatch, s
         never resolved as a literal relative directory.
         """
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setenv("TERMINAL_CWD", sentinel)
     
         assert ft._configured_terminal_cwd() is None
    ```

    ```diff
    @@ -261,7 +261,7 @@ def test_relative_nonsentinel_terminal_cwd_rejected(_isolated_cwd, monkeypatch):
         be joined onto it as a literal subdir.
         """
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setenv("TERMINAL_CWD", "some/rel/path")
     
         assert ft._configured_terminal_cwd() is None
    ```

    ```diff
    @@ -277,7 +277,7 @@ def test_absolute_terminal_cwd_anchors_with_empty_registry(_isolated_cwd, monkey
         worktree — not the process cwd (main repo).
         """
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setenv("TERMINAL_CWD", str(workspace))
     
         resolved = ft._resolve_path_for_task("target.py", task_id="default")
    ```

    ```diff
    @@ -295,7 +295,7 @@ def test_registered_task_cwd_override_anchors_before_terminal_env_exists(_isolat
         """
         workspace, decoy = _isolated_cwd
         task_id = "desktop-session-cwd"
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.delenv("TERMINAL_CWD", raising=False)
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
     
    ```

    ```diff
    @@ -317,7 +317,7 @@ def test_warning_fires_from_terminal_cwd_when_registry_empty(_isolated_cwd, monk
         worktree is flagged on the very first write.
         """
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setenv("TERMINAL_CWD", str(workspace))
     
         # Relative path that escapes the worktree into the decoy/main checkout.
    ```

    ```diff
    @@ -336,8 +336,8 @@ def test_live_cwd_still_wins_over_absolute_terminal_cwd(_isolated_cwd, monkeypat
         workspace, decoy = _isolated_cwd
         other = decoy.parent / "other"
         other.mkdir()
    -    # Live cwd = workspace; TERMINAL_CWD points elsewhere — live must win.
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    # Recorded session cwd = workspace; TERMINAL_CWD points elsewhere — record wins.
    +    terminal_tool.record_session_cwd("default", str(workspace))
         monkeypatch.setenv("TERMINAL_CWD", str(other))
     
         resolved = ft._resolve_path_for_task("target.py", task_id="default")
    ```

    ```diff
    @@ -351,7 +351,7 @@ def test_live_cwd_still_wins_over_absolute_terminal_cwd(_isolated_cwd, monkeypat
     def test_write_file_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):
         """write_file_tool must put the absolute on-disk path in files_modified."""
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    terminal_tool.record_session_cwd("t1", str(workspace))
     
         import json
         out = json.loads(ft.write_file_tool("newfile.txt", "hello\n", task_id="t1"))
    ```

    ```diff
    @@ -365,7 +365,7 @@ def test_write_file_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):
     def test_patch_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):
         """patch_tool (replace mode) must put the absolute on-disk path in files_modified."""
         workspace, decoy = _isolated_cwd
    -    monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": str(workspace))
    +    terminal_tool.record_session_cwd("t1", str(workspace))
     
         import json
         out = json.loads(ft.patch_tool(
    ```

    ```diff
    @@ -383,24 +383,16 @@ def test_patch_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):
         assert (decoy / "target.py").read_text() == "DECOY_ORIGINAL\n"
     
     
    -# ── Fix D: shared terminal env must not leak its cwd across worktree sessions ─
    -# (June 2026: two desktop sessions, each on its own worktree, share the single
    -# "default" terminal environment. Its `cwd` tracks whichever session ran the
    -# last command, so a file edit from the OTHER session resolved against that
    -# foreign cwd and silently landed in the wrong worktree. terminal_tool now
    -# stamps env.cwd_owner with the driving session; file tools trust the shared
    -# env's live cwd only when the resolving session owns it.)
    -
    -
    -class _FakeOwnedEnv:
    -    def __init__(self, cwd: str, cwd_owner: str):
    -        self.cwd = cwd
    -        self.cwd_owner = cwd_owner
    +# ── Cross-session isolation: one session's cwd never leaks into another ──────
    +# (June 2026 bug class: two desktop sessions, each on its own worktree, shared
    +# the single "default" terminal environment and could inherit each other's cwd.
    +# The per-session record store solves this structurally: each session's cd
    +# state lives in its own record, keyed by the raw session id.)
     
     
     @pytest.fixture
     def _two_worktree_sessions(tmp_path, monkeypatch):
    -    """Two worktree sessions sharing one terminal env owned by session B."""
    +    """Two worktree sessions: B has cd'd (record), both registered overrides."""
         wt_a = tmp_path / "wt_a"
         wt_b = tmp_path / "wt_b"
         main = tmp_path / "main"
    ```

    ```diff
    @@ -410,78 +402,60 @@ def _two_worktree_sessions(tmp_path, monkeypatch):
         monkeypatch.chdir(main)
         monkeypatch.delenv("TERMINAL_CWD", raising=False)
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setattr(ft, "_file_ops_cache", {})
    -    # Both sessions register their worktree cwd (TUI/desktop registration path).
    +    # Both sessions register their worktree cwd (TUI/desktop registration path;
    +    # registration seeds each session's record).
         terminal_tool.register_task_env_overrides("sess-a", {"cwd": str(wt_a)})
         terminal_tool.register_task_env_overrides("sess-b", {"cwd": str(wt_b)})
    -    # The shared "default" env: session B ran the last command, so its live cwd
    -    # is wt_b and B owns it.
    +    # Session B ran the last command; the shared env's live cwd is wt_b but
    +    # only B's RECORD carries it.
         monkeypatch.setattr(
             terminal_tool,
             "_active_environments",
    -        {"default": _FakeOwnedEnv(str(wt_b), "sess-b")},
    +        {"default": _FakeEnv(str(wt_b))},
         )
         return wt_a, wt_b, main
     
     
    -def test_live_cwd_ignored_for_non_owning_session(_two_worktree_sessions):
    -    wt_a, wt_b, _main = _two_worktree_sessions
    -    # Owner sees the live cwd; the other session must NOT inherit it.
    -    assert ft._get_live_tracking_cwd("sess-b") == str(wt_b)
    -    assert ft._get_live_tracking_cwd("sess-a") is None
    +class _FakeEnv:
    +    def __init__(self, cwd: str):
    +        self.cwd = cwd
     
     
     def test_resolution_routes_to_resolving_sessions_worktree(_two_worktree_sessions):
         """The wrong-worktree fix: A resolves into wt_a, not the shared env's wt_b."""
         wt_a, wt_b, _main = _two_worktree_sessions
    -    # Session A does not own the shared env → falls back to its own registered
    -    # worktree cwd instead of B's live cwd.
         resolved_a = ft._resolve_path_for_task("target.py", task_id="sess-a")
         assert resolved_a == (wt_a / "target.py")
         assert not str(resolved_a).startswith(str(wt_b))
     
     
    -def test_owning_session_still_resolves_against_live_cwd(_two_worktree_sessions):
    -    """No regression: the owner keeps resolving against the live cwd."""
    +def test_session_with_cd_record_resolves_against_it(_two_worktree_sessions):
    +    """B's record (its own cd state) is authoritative for B."""
         wt_a, wt_b, _main = _two_worktree_sessions
         resolved_b = ft._resolve_path_for_task("target.py", task_id="sess-b")
         assert resolved_b == (wt_b / "target.py")
         assert not str(resolved_b).startswith(str(wt_a))
     
     
    -def test_unknown_owner_keeps_prior_single_session_behavior(tmp_path, monkeypatch):
    -    """An env with no owner (CLI / legacy) still yields its live cwd."""
    -    ws = tmp_path / "ws"
    -    ws.mkdir()
    -    monkeypatch.setattr(ft, "_file_ops_cache", {})
    -    monkeypatch.setattr(
    -        terminal_tool,
    -        "_active_environments",
    -        {"default": _FakeOwnedEnv(str(ws), "")},
    -    )
    -    assert ft._get_live_tracking_cwd("default") == str(ws)
    -    assert ft._get_live_tracking_cwd("any-session") == str(ws)
    +def test_sessions_cd_updates_only_its_own_resolution(_two_worktree_sessions, tmp_path):
    +    """B cd's elsewhere → B's resolution follows, A's is untouched."""
    +    wt_a, wt_b, _main = _two_worktree_sessions
    +    elsewhere = tmp_path / "elsewhere"
    +    elsewhere.mkdir()
    +    terminal_tool.record_session_cwd("sess-b", str(elsewhere))
    +
    +    assert ft._resolve_path_for_task("f.py", task_id="sess-b") == (elsewhere / "f.py")
    +    assert ft._resolve_path_for_task("f.py", task_id="sess-a") == (wt_a / "f.py")
     
     
    -def test_preserved_cwd_does_not_override_non_owning_sessions_worktree(
    +def test_unregistered_session_never_inherits_another_sessions_record(
         _two_worktree_sessions, monkeypatch
     ):
    -    """#26211 belt-and-suspenders must not break worktree isolation.
    -
    -    The owner (session B) doing an owned live read mirrors wt_b into the shared
    -    _last_known_cwd['default'] registry. Session A — which does NOT own the env
    -    but HAS its own registered worktree (wt_a) — must still resolve into wt_a,
    -    not inherit B's preserved cwd through the shared-container key. The
    -    session-specific registered override must beat the durable shared anchor.
    -    """
    -    wt_a, wt_b, _main = _two_worktree_sessions
    -    monkeypatch.setattr(ft, "_last_known_cwd", {})
    -
    -    # Owner B resolves first — this mirrors wt_b into _last_known_cwd['default'].
    -    assert ft._resolve_path_for_task("target.py", task_id="sess-b") == (wt_b / "target.py")
    -    assert ft._last_known_cwd.get("default") == str(wt_b)
    -
    -    # A still routes to its own registered worktree despite the shared anchor.
    -    resolved_a = ft._resolve_path_for_task("target.py", task_id="sess-a")
    -    assert resolved_a == (wt_a / "target.py")
    -    assert not str(resolved_a).startswith(str(wt_b))
    +    """Session C: no record, no override. Must NOT inherit A's or B's cwd."""
    +    wt_a, wt_b, main = _two_worktree_sessions
    +    resolved = ft._resolve_path_for_task("target.py", task_id="sess-c")
    +    assert not str(resolved).startswith(str(wt_a))
    +    assert not str(resolved).startswith(str(wt_b))
    +    assert resolved == (main / "target.py").resolve()
    ```
  - `tests/tools/test_file_tools_tilde_profile.py`

    ```diff
    @@ -21,6 +21,7 @@
     import pytest
     
     import tools.file_tools as ft
    +import tools.terminal_tool as terminal_tool
     
     
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -82,7 +83,7 @@ def test_relative_tilde_resolves_to_profile_home(self, tmp_path, monkeypatch):
             process_home.mkdir()
     
             monkeypatch.setenv("HOME", str(process_home))
    -        monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +        monkeypatch.setattr(terminal_tool, "_session_cwd", {})
     
             with patch("hermes_constants.get_subprocess_home", return_value=str(profile_home)):
                 resolved = ft._resolve_path_for_task("~/test_file.txt", task_id="test")
    ```

    ```diff
    @@ -98,7 +99,7 @@ def test_absolute_tilde_in_workspace_root(self, tmp_path, monkeypatch):
             process_home.mkdir()
     
             monkeypatch.setenv("HOME", str(process_home))
    -        monkeypatch.setattr(ft, "_get_live_tracking_cwd", lambda task_id="default": None)
    +        monkeypatch.setattr(terminal_tool, "_session_cwd", {})
     
             with patch("hermes_constants.get_subprocess_home", return_value=str(profile_home)):
                 # _resolve_base_dir uses the workspace root from config; if it contains ~,
    ```
  - `tests/tools/test_resolve_path.py`

    ```diff
    @@ -52,33 +52,23 @@ def test_result_is_resolved(self, monkeypatch, tmp_path):
             assert ".." not in str(result)
             assert result == (tmp_path / "b" / "file.txt")
     
    -    def test_relative_path_prefers_live_file_ops_cwd(self, monkeypatch, tmp_path):
    -        """Live env.cwd must win after the terminal session changes directory."""
    +    def test_relative_path_prefers_recorded_session_cwd(self, monkeypatch, tmp_path):
    +        """The session's recorded cwd must win after the terminal changes directory."""
             start_dir = tmp_path / "start"
             live_dir = tmp_path / "worktree"
             start_dir.mkdir()
             live_dir.mkdir()
             monkeypatch.setenv("TERMINAL_CWD", str(start_dir))
     
    -        from tools import file_tools
    +        from tools import file_tools, terminal_tool
     
             task_id = "live-cwd"
    -        fake_ops = SimpleNamespace(
    -            env=SimpleNamespace(cwd=str(live_dir)),
    -            cwd=str(start_dir),
    -        )
    -
    -        with file_tools._file_ops_lock:
    -            previous = file_tools._file_ops_cache.get(task_id)
    -            file_tools._file_ops_cache[task_id] = fake_ops
    +        # The session's completed `cd` recorded the new directory.
    +        terminal_tool.record_session_cwd(task_id, str(live_dir))
     
             try:
                 result = file_tools._resolve_path("nested/file.txt", task_id=task_id)
             finally:
    -            with file_tools._file_ops_lock:
    -                if previous is None:
    -                    file_tools._file_ops_cache.pop(task_id, None)
    -                else:
    -                    file_tools._file_ops_cache[task_id] = previous
    +            terminal_tool.clear_session_cwd(task_id)
     
             assert result == live_dir / "nested" / "file.txt"
    ```
  - `tests/tools/test_session_cwd_store.py`

    ```diff
    @@ -0,0 +1,244 @@
    +"""Session-cwd record store (cwd rearchitecture, step 1: dual-write).
    +
    +The store is the future single source of truth for per-session working
    +directories. Step 1 only guarantees the WRITE side: every path that learns a
    +session's live cwd must record it under the raw session key. Readers still
    +use the legacy env.cwd ladder; these tests pin the invariants the later
    +read-side flip will rely on.
    +"""
    +
    +import pytest
    +
    +import tools.terminal_tool as tt
    +
    +
    [redacted:email](autouse=True)
    +def _clean_store(monkeypatch):
    +    monkeypatch.setattr(tt, "_session_cwd", {})
    +    monkeypatch.setattr(tt, "_task_env_overrides", {})
    +
    +
    +class TestRecordSemantics:
    +    def test_records_are_keyed_by_raw_session_key(self):
    +        tt.record_session_cwd("sess-a", "/wt/a")
    +        tt.record_session_cwd("sess-b", "/wt/b")
    +        # No cross-talk: each session reads back exactly its own record.
    +        assert tt.get_session_cwd("sess-a") == "/wt/a"
    +        assert tt.get_session_cwd("sess-b") == "/wt/b"
    +        assert tt.get_session_cwd("sess-c") is None
    +
    +    def test_none_and_empty_keys_collapse_to_default(self):
    +        tt.record_session_cwd(None, "/somewhere")
    +        assert tt.get_session_cwd(None) == "/somewhere"
    +        assert tt.get_session_cwd("") == "/somewhere"
    +        assert tt.get_session_cwd("default") == "/somewhere"
    +
    +    def test_invalid_cwd_values_are_ignored(self):
    +        tt.record_session_cwd("sess-a", None)
    +        tt.record_session_cwd("sess-a", "")
    +        tt.record_session_cwd("sess-a", "   ")
    +        tt.record_session_cwd("sess-a", 123)  # type: ignore[arg-type]
    +        assert tt.get_session_cwd("sess-a") is None
    +
    +    def test_clear_drops_only_the_named_session(self):
    +        tt.record_session_cwd("sess-a", "/wt/a")
    +        tt.record_session_cwd("sess-b", "/wt/b")
    +        tt.clear_session_cwd("sess-a")
    +        assert tt.get_session_cwd("sess-a") is None
    +        assert tt.get_session_cwd("sess-b") == "/wt/b"
    +
    +
    +class TestDualWriteSites:
    +    def test_register_cwd_override_seeds_the_session_record(self):
    +        """A registered workspace cwd IS the session's cwd until a `cd`."""
    +        tt.register_task_env_overrides("desktop-sess", {"cwd": "/wt/desktop"})
    +        assert tt.get_session_cwd("desktop-sess") == "/wt/desktop"
    +
    +    def test_register_without_cwd_does_not_touch_the_record(self):
    +        tt.register_task_env_overrides("rl-42", {"docker_image": "x:y"})
    +        assert tt.get_session_cwd("rl-42") is None
    +
    +    def test_clear_task_env_overrides_drops_the_record(self):
    +        tt.register_task_env_overrides("desktop-sess", {"cwd": "/wt/desktop"})
    +        tt.clear_task_env_overrides("desktop-sess")
    +        assert tt.get_session_cwd("desktop-sess") is None
    +
    +    def test_reregistration_updates_the_record(self):
    +        """ACP session/load switching project roots mid-session."""
    +        tt.register_task_env_overrides("acp-sess", {"cwd": "/proj/one"})
    +        tt.register_task_env_overrides("acp-sess", {"cwd": "/proj/two"})
    +        assert tt.get_session_cwd("acp-sess") == "/proj/two"
    +
    +
    +class TestPostCommandDualWrite:
    +    """The env's post-command cwd tracking must mirror into the session record."""
    +
    +    def _run(self, monkeypatch, task_id, env):
    +        import json
    +        monkeypatch.setattr(tt, "_active_environments", {task_id: env})
    +        monkeypatch.setattr(tt, "_last_activity", {})
    +        monkeypatch.setattr(
    +            tt, "_get_env_config",
    +            lambda: {"env_type": "local", "cwd": "/default", "timeout": 60,
    +                     "lifetime_seconds": 3600},
    +        )
    +        monkeypatch.setattr(
    +            tt, "_check_all_guards",
    +            lambda command, env_type, **kwargs: {"approved": True},
    +        )
    +        return json.loads(tt.terminal_tool(command="cd /new/dir", task_id=task_id))
    +
    +    def test_cd_result_is_recorded_under_the_session_key(self, monkeypatch):
    +        class FakeEnv:
    +            env = {}
    +            cwd = "/start"
    +            def execute(self, command, **kwargs):
    +                # Simulate the env's own post-command tracking (marker parse).
    +                self.cwd = "/new/dir"
    +                return {"output": "", "returncode": 0}
    +
    +        result = self._run(monkeypatch, "sess-a", FakeEnv())
    +        assert result["exit_code"] == 0
    +        assert tt.get_session_cwd("sess-a") == "/new/dir"
    +        # And ONLY that session's record was touched.
    +        assert tt.get_session_cwd("sess-b") is None
    +
    +    def test_envs_without_cwd_tracking_record_nothing(self, monkeypatch):
    +        class FakeEnv:
    +            env = {}
    +            def execute(self, command, **kwargs):
    +                return {"output": "", "returncode": 0}
    +
    +        result = self._run(monkeypatch, "sess-a", FakeEnv())
    +        assert result["exit_code"] == 0
    +        assert tt.get_session_cwd("sess-a") is None
    +
    +
    +class TestFileToolsReadTheRecord:
    +    """Step 2: file-tool path resolution prefers the session's own record."""
    +
    +    def test_two_sessions_resolve_into_their_own_recorded_cwds(self, tmp_path, monkeypatch):
    +        import tools.file_tools as ft
    +
    +        wt_a = tmp_path / "wt_a"
    +        wt_b = tmp_path / "wt_b"
    +        for d in (wt_a, wt_b):
    +            d.mkdir()
    +        monkeypatch.chdir(tmp_path)
    +        monkeypatch.delenv("TERMINAL_CWD", raising=False)
    +        monkeypatch.setattr(ft, "_file_ops_cache", {})
    +        monkeypatch.setattr(tt, "_active_environments", {})
    +
    +        # Each session ran commands that recorded its own cwd. No env alive,
    +        # no registered overrides — just the records.
    +        tt.record_session_cwd("sess-a", str(wt_a))
    +        tt.record_session_cwd("sess-b", str(wt_b))
    +
    +        assert ft._resolve_path_for_task("f.py", task_id="sess-a") == (wt_a / "f.py")
    +        assert ft._resolve_path_for_task("f.py", task_id="sess-b") == (wt_b / "f.py")
    +
    +    def test_record_beats_foreign_env_cwd_without_ownership_metadata(self, tmp_path, monkeypatch):
    +        """The leak-A scenario, solved structurally: the shared env's cwd is
    +        never consulted for path resolution — only the session's own record."""
    +        import tools.file_tools as ft
    +
    +        wt_a = tmp_path / "wt_a"
    +        wt_b = tmp_path / "wt_b"
    +        for d in (wt_a, wt_b):
    +            d.mkdir()
    +        monkeypatch.chdir(tmp_path)
    +        monkeypatch.delenv("TERMINAL_CWD", raising=False)
    +        monkeypatch.setattr(ft, "_file_ops_cache", {})
    +
    +        class _Env:
    +            cwd = str(wt_b)  # another session's leftover cd on the shared env
    +
    +        monkeypatch.setattr(tt, "_active_environments", {"default": _Env()})
    +        tt.record_session_cwd("sess-a", str(wt_a))
    +
    +        resolved = ft._resolve_path_for_task("f.py", task_id="sess-a")
    +        assert resolved == (wt_a / "f.py")
    +        assert not str(resolved).startswith(str(wt_b))
    +
    +
    +class TestDelegateSeedsChildRecord:
    +    def test_child_record_seeded_from_parent_then_isolated(self):
    +        tt.record_session_cwd("parent-task", "/parent/worktree")
    +        # what delegate_tool does at spawn:
    +        tt.record_session_cwd("child-1", tt.get_session_cwd("parent-task"))
    +
    +        assert tt.get_session_cwd("child-1") == "/parent/worktree"
    +        # child cds somewhere; parent record must be untouched.
    +        tt.record_session_cwd("child-1", "/child/scratch")
    +        assert tt.get_session_cwd("parent-task") == "/parent/worktree"
    +        assert tt.get_session_cwd("child-1") == "/child/scratch"
    +
    +
    +class TestCommandCwdReadsTheRecord:
    +    """_resolve_command_cwd: workdir > session record > default. Nothing else."""
    +
    +    def test_record_beats_default(self):
    +        tt.record_session_cwd("sess-a", "/my/worktree")
    +        resolved = tt._resolve_command_cwd(
    +            workdir=None,
    +            default_cwd="/config/default",
    +            session_key="sess-a",
    +        )
    +        assert resolved == "/my/worktree"
    +
    +    def test_workdir_still_beats_the_record(self):
    +        tt.record_session_cwd("sess-a", "/my/worktree")
    +        resolved = tt._resolve_command_cwd(
    +            workdir="/explicit/place",
    +            default_cwd="/config/default",
    +            session_key="sess-a",
    +        )
    +        assert resolved == "/explicit/place"
    +
    +    def test_no_record_falls_back_to_default(self):
    +        resolved = tt._resolve_command_cwd(
    +            workdir=None,
    +            default_cwd="/config/default",
    +            session_key="sess-a",
    +        )
    +        assert resolved == "/config/default"
    +
    +    def test_other_sessions_record_is_not_consulted(self):
    +        tt.record_session_cwd("sess-b", "/other/worktree")
    +        resolved = tt._resolve_command_cwd(
    +            workdir=None,
    +            default_cwd="/config/default",
    +            session_key="sess-a",
    +        )
    +        assert resolved == "/config/default"
    +
    +    def test_cd_then_next_command_runs_in_the_new_dir(self, monkeypatch):
    +        """E2E through terminal_tool: the record round-trips cd state."""
    +        import json
    +
    +        class FakeEnv:
    +            env = {}
    +            cwd = "/start"
    +            def execute(self, command, **kwargs):
    +                self.last_cwd_arg = kwargs.get("cwd")
    +                if command.startswith("cd "):
    +                    self.cwd = command[3:]
    +                return {"output": "", "returncode": 0}
    +
    +        fake = FakeEnv()
    +        monkeypatch.setattr(tt, "_active_environments", {"sess-a": fake})
    +        monkeypatch.setattr(tt, "_last_activity", {})
    +        monkeypatch.setattr(
    +            tt, "_get_env_config",
    +            lambda: {"env_type": "local", "cwd": "/default", "timeout": 60,
    +                     "lifetime_seconds": 3600},
    +        )
    +        monkeypatch.setattr(
    +            tt, "_check_all_guards",
    +            lambda command, env_type, **kwargs: {"approved": True},
    +        )
    +
    +        json.loads(tt.terminal_tool(command="cd /project", task_id="sess-a"))
    +        assert tt.get_session_cwd("sess-a") == "/project"
    +        json.loads(tt.terminal_tool(command="pwd", task_id="sess-a"))
    +        assert fake.last_cwd_arg == "/project"
    ```
  - `tests/tools/test_terminal_task_cwd.py`

    ```diff
    @@ -76,8 +76,8 @@ def execute(self, command, **kwargs):
         assert calls == [{"timeout": 60, "cwd": "/explicit/workdir", "bounded_capture": True}]
     
     
    -def test_foreground_command_prefers_live_env_cwd_over_init_time_cwd(monkeypatch):
    -    """A prior `cd` updates env.cwd; terminal_tool must honor that live cwd."""
    +def test_foreground_command_prefers_recorded_session_cwd_over_init_time_cwd(monkeypatch):
    +    """A prior `cd` records the session cwd; terminal_tool must honor it."""
         calls = []
     
         class FakeEnv:
    ```

    ```diff
    @@ -91,6 +91,7 @@ def execute(self, command, **kwargs):
         task_id = "session-live-cwd"
         monkeypatch.setattr(terminal_tool, "_active_environments", {task_id: FakeEnv()})
         monkeypatch.setattr(terminal_tool, "_last_activity", {})
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {task_id: {"cwd": "/workspace/init"}})
         monkeypatch.setattr(terminal_tool, "_get_env_config", lambda: _minimal_terminal_config(cwd="/workspace/init"))
         monkeypatch.setattr(terminal_tool, "_start_cleanup_thread", lambda: None)
    ```

    ```diff
    @@ -100,6 +101,8 @@ def execute(self, command, **kwargs):
             "_check_all_guards",
             lambda command, env_type, **kwargs: {"approved": True},
         )
    +    # The prior command's completed `cd` recorded the session cwd.
    +    terminal_tool.record_session_cwd(task_id, "/workspace/live")
     
         result = json.loads(terminal_tool.terminal_tool(command="pwd", task_id=task_id))
     
    ```

    ```diff
    @@ -107,8 +110,8 @@ def execute(self, command, **kwargs):
         assert calls == [("pwd", {"timeout": 60, "cwd": "/workspace/live", "bounded_capture": True})]
     
     
    -def test_background_command_prefers_live_env_cwd_over_init_time_cwd(monkeypatch):
    -    """Background process launches must also use the live session cwd."""
    +def test_background_command_prefers_recorded_session_cwd_over_init_time_cwd(monkeypatch):
    +    """Background process launches must also use the recorded session cwd."""
     
         class FakeEnv:
             env = {}
    ```

    ```diff
    @@ -129,6 +132,7 @@ def spawn_local(self, **kwargs):
         task_id = "session-live-cwd-bg"
         monkeypatch.setattr(terminal_tool, "_active_environments", {task_id: FakeEnv()})
         monkeypatch.setattr(terminal_tool, "_last_activity", {})
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {task_id: {"cwd": "/workspace/init"}})
         monkeypatch.setattr(terminal_tool, "_get_env_config", lambda: _minimal_terminal_config(cwd="/workspace/init"))
         monkeypatch.setattr(terminal_tool, "_start_cleanup_thread", lambda: None)
    ```

    ```diff
    @@ -139,6 +143,7 @@ def spawn_local(self, **kwargs):
             lambda command, env_type, **kwargs: {"approved": True},
         )
         monkeypatch.setattr(process_registry_mod, "process_registry", registry)
    +    terminal_tool.record_session_cwd(task_id, "/workspace/live")
     
         result = json.loads(
             terminal_tool.terminal_tool(
    ```

    ```diff
    @@ -162,15 +167,13 @@ def spawn_local(self, **kwargs):
         }]
     
     
    -def test_registering_cwd_override_updates_live_env_cwd(monkeypatch):
    +def test_registering_cwd_override_updates_session_record(monkeypatch):
         """An ACP ``update_cwd`` (re-)registered mid-session must win over a
    -    previously ``cd``-ed live ``env.cwd``.
    +    previously ``cd``-ed session cwd.
     
    -    Preferring live ``env.cwd`` (so session-local ``cd`` survives) means a
    -    freshly registered ``cwd`` override would otherwise sit *below* the
    -    already-set ``env.cwd`` and be silently ignored. ``register_task_env_overrides``
    -    syncs the new cwd onto the live cached env so an explicit ACP project-root
    -    change takes effect, as the editor client expects.
    +    Registration writes the session record directly, so an explicit ACP
    +    project-root change takes effect on the next command, as the editor
    +    client expects.
         """
     
         class FakeEnv:
    ```

    ```diff
    @@ -181,15 +184,18 @@ class FakeEnv:
         fake_env = FakeEnv()
         monkeypatch.setattr(terminal_tool, "_active_environments", {task_id: fake_env})
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
    +    # The session had cd'd somewhere before the editor switched project roots.
    +    terminal_tool.record_session_cwd(task_id, "/workspace/old")
     
         terminal_tool.register_task_env_overrides(task_id, {"cwd": "/workspace/new"})
     
    -    # The live env now reflects the editor's new project root.
    +    # The live env mirror still updates (legacy env seeding) …
         assert fake_env.cwd == "/workspace/new"
    -
    -    # A subsequent command resolves to the new cwd (env.cwd precedence).
    +    # … and the session record — what commands actually resolve against — too.
    +    assert terminal_tool.get_session_cwd(task_id) == "/workspace/new"
         assert terminal_tool._resolve_command_cwd(
    -        workdir=None, env=fake_env, default_cwd="/workspace/config"
    +        workdir=None, default_cwd="/workspace/config", session_key=task_id
         ) == "/workspace/new"
     
     
    ```

    ```diff
    @@ -264,20 +270,14 @@ def execute(self, command, **kwargs):
         assert calls == [("pwd", {"timeout": 60, "cwd": "/home/user/src/hermes-agent", "bounded_capture": True})]
     
     
    -def test_same_session_env_cwd_is_trusted_after_first_claim(monkeypatch):
    -    """Once a session has claimed the env, subsequent commands trust env.cwd.
    -
    -    The prev_owner check only rejects env.cwd when a DIFFERENT session owned it
    -    before this call.  After the first command (which claims ownership),
    -    subsequent calls in the same session should trust the live env.cwd so that
    -    in-session `cd` state survives.
    -    """
    +def test_same_session_recorded_cwd_survives_across_commands(monkeypatch):
    +    """In-session `cd` state survives: the record written by one command is
    +    used by the next command in the same session."""
         calls = []
     
         class FakeEnv:
             env = {}
             cwd = "/workspace/deep"
    -        cwd_owner = "session-X"
     
             def execute(self, command, **kwargs):
                 calls.append((command, kwargs))
    ```

    ```diff
    @@ -288,6 +288,7 @@ def execute(self, command, **kwargs):
         monkeypatch.setattr(terminal_tool, "_active_environments", {"default": env})
         monkeypatch.setattr(terminal_tool, "_last_activity", {})
         monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
    +    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
         monkeypatch.setattr(terminal_tool, "_get_env_config", lambda: _minimal_terminal_config(cwd="/workspace/config"))
         monkeypatch.setattr(terminal_tool, "_start_cleanup_thread", lambda: None)
         monkeypatch.setattr(terminal_tool, "_resolve_container_task_id", lambda value: "default")
    ```

    ```diff
    @@ -297,13 +298,17 @@ def execute(self, command, **kwargs):
             lambda command, env_type, **kwargs: {"approved": True},
         )
     
    -    # First call: env was owned by "session-X" (same session_key since
    -    # get_current_session_key falls back to task_id). prev_owner == current
    -    # session, so env.cwd is trusted.
    +    # First command runs in the config cwd (no record yet) and afterwards
    +    # mirrors the env's post-command cwd into the session record.
         result = json.loads(terminal_tool.terminal_tool(command="pwd", task_id=task_id))
    +    assert result["exit_code"] == 0
    +    assert calls[0] == ("pwd", {"timeout": 60, "cwd": "/workspace/config", "bounded_capture": True})
    +    assert terminal_tool.get_session_cwd(task_id) == "/workspace/deep"
     
    +    # Second command in the same session trusts the record.
    +    result = json.loads(terminal_tool.terminal_tool(command="pwd", task_id=task_id))
         assert result["exit_code"] == 0
    -    assert calls == [("pwd", {"timeout": 60, "cwd": "/workspace/deep", "bounded_capture": True})]
    +    assert calls[1] == ("pwd", {"timeout": 60, "cwd": "/workspace/deep", "bounded_capture": True})
     
     
     def test_safe_getcwd_returns_real_cwd(monkeypatch):
    ```
  - `tools/delegate_tool.py`

    ```diff
    @@ -1906,6 +1906,18 @@ def _heartbeat_loop():
     
             child_task_id = _subagent_id or f"subagent-{task_index}-{_uuid.uuid4().hex[:8]}"
             parent_task_id = getattr(parent_agent, "_current_task_id", None)
    +        # Seed the child's session-cwd record from the parent's (cwd rearch):
    +        # children share the parent's container, and today they inherit the
    +        # parent's live env.cwd implicitly. Seeding at spawn preserves that
    +        # starting directory while keeping the child's subsequent `cd`s
    +        # isolated in its own record (a child's cd no longer bleeds back into
    +        # the parent once readers flip to the record store).
    +        try:
    +            from tools.terminal_tool import get_session_cwd, record_session_cwd
    +
    +            record_session_cwd(child_task_id, get_session_cwd(parent_task_id))
    +        except Exception as e:
    +            logger.debug("Child cwd seed failed: %s", e)
             wall_start = time.time()
             parent_reads_snapshot = (
                 list(file_state.known_reads(parent_task_id)) if parent_task_id else []
    ```
  - `tools/file_tools.py`

    ```diff
    @@ -269,103 +269,37 @@ def _registered_task_cwd_override(task_id: str = "default") -> str | None:
         return _sentinel_free_abs_cwd(overrides.get("cwd"))
     
     
    -def _live_cwd_if_owned(env, task_id: str) -> str | None:
    -    """The env's live cwd, but only when THIS session owns it.
    -
    -    The terminal env is shared (collapsed to the ``"default"`` container), so its
    -    ``cwd`` tracks the LAST session that ran a command. With two worktree
    -    sessions open, trusting it blindly routes one session's edits into the other
    -    session's checkout (the wrong-worktree-patch bug). ``terminal_tool`` stamps
    -    ``env.cwd_owner`` with the session that last drove the env; return its cwd
    -    only when that owner matches the resolving session, else ``None`` so the
    -    caller falls through to this session's own registered cwd override. Unknown
    -    owner / ``default`` keys keep the prior behavior (single-session / CLI).
    -    """
    -    if env is None:
    -        return None
    -    live = getattr(env, "cwd", None)
    -    if not live:
    -        return None
    -    owner = str(getattr(env, "cwd_owner", "") or "")
    -    tid = str(task_id or "")
    -    if owner and tid and owner != "default" and tid != "default" and owner != tid:
    -        return None
    -    return live
    -
    -
    -def _get_live_tracking_cwd(task_id: str = "default") -> str | None:
    -    """Return the task's live terminal cwd for bookkeeping when available."""
    -    try:
    -        from tools.terminal_tool import _resolve_container_task_id
    -        container_key = _resolve_container_task_id(task_id)
    -    except Exception:
    -        container_key = task_id
    -
    -    with _file_ops_lock:
    -        cached = _file_ops_cache.get(container_key) or _file_ops_cache.get(task_id)
    -    if cached is not None:
    -        env = getattr(cached, "env", None)
    -        live_cwd = _live_cwd_if_owned(env, task_id)
    -        if live_cwd:
    -            _remember_last_known_cwd(container_key, live_cwd)
    -            return live_cwd
    -        # Legacy: a cache entry carrying its own cwd with no env to own it.
    -        if env is None and getattr(cached, "cwd", None):
    -            legacy_cwd = getattr(cached, "cwd", None)
    -            _remember_last_known_cwd(container_key, legacy_cwd)
    -            return legacy_cwd
    -
    -    try:
    -        from tools.terminal_tool import _active_environments, _env_lock
    -
    -        with _env_lock:
    -            env = _active_environments.get(container_key) or _active_environments.get(task_id)
    -        live_cwd = _live_cwd_if_owned(env, task_id)
    -        if live_cwd:
    -            _remember_last_known_cwd(container_key, live_cwd)
    -            return live_cwd
    -    except Exception:
    -        pass
    -
    -    return None
    -
    -
     def _authoritative_workspace_root(task_id: str = "default") -> str | None:
         """Best-effort absolute workspace root for divergence checks.
     
    -    Prefers the live terminal cwd (the directory the agent is actually working
    -    in). When no terminal command has run yet — so the live registry is empty —
    -    falls back to a registered task/session cwd override (TUI/Desktop/ACP
    -    sessions register a raw-keyed cwd before any tool runs), then to a
    -    sentinel-free absolute ``$TERMINAL_CWD``. This is what lets a worktree or
    -    Desktop session warn about (and resolve into) its workspace from the very
    -    first ``write_file``/``patch``, before any ``cd`` has populated the live cwd.
    +    Resolution:
    +
    +      1. The session's own cwd RECORD (``terminal_tool.get_session_cwd``) —
    +         written on every completed terminal command and seeded by workspace
    +         registration, keyed by the raw session id. Because the record is
    +         per-session, one session's ``cd`` can never leak into another
    +         session's resolution.
    +      2. A registered task/session cwd override (TUI/Desktop/ACP sessions
    +         register a raw-keyed cwd before any tool runs). Normally already
    +         mirrored into the record at registration; kept as a direct fallback
    +         so a cleared/never-written record still resolves the workspace.
    +      3. A sentinel-free absolute ``$TERMINAL_CWD`` (the worktree path set by
    +         ``cli.py``/``main.py`` for ``-w`` sessions).
     
         Returns ``None`` only when there is genuinely no reliable anchor, in which
         case callers fall back to the process cwd.
         """
    -    live = _get_live_tracking_cwd(task_id)
    -    if live:
    -        return live
    -    # A session-specific registered override (TUI/Desktop/ACP workspace cwd)
    -    # is more authoritative than the shared last-known anchor: it is keyed by
    -    # the raw session id, so when two worktree sessions share the single
    -    # "default" terminal env, a NON-owning session must resolve against its OWN
    -    # registered worktree — never the other session's leftover cwd. (Checked
    -    # before _last_known_cwd, which is keyed by the shared container id.)
    +    try:
    +        from tools.terminal_tool import get_session_cwd
    +
    +        recorded = get_session_cwd(task_id)
    +    except Exception:
    +        recorded = None
    +    if recorded:
    +        return recorded
         registered = _registered_task_cwd_override(task_id)
         if registered:
             return registered
    -    # When the terminal env was cleaned up mid-conversation, the live cwd is
    -    # gone but the directory the agent navigated to is still recorded in the
    -    # durable _last_known_cwd registry. Prefer it over the config/process
    -    # fallback so a relative-path write resolved BEFORE the env is rebuilt
    -    # still lands in the user's directory (root cause of #26211: write happens
    -    # via _resolve_path_for_task -> here, which runs before _get_file_ops
    -    # rebuilds the env). Keyed by the resolved container id, same as the save.
    -    preserved = _last_known_cwd_for(task_id)
    -    if preserved:
    -        return preserved
         return _configured_terminal_cwd()
     
     
    ```

    ```diff
    @@ -794,45 +728,6 @@ def _is_expected_write_exception(exc: Exception) -> bool:
     
     _file_ops_lock = threading.Lock()
     _file_ops_cache: dict = {}
    -# Per-task last-known CWD — preserved across env re-creation so
    -# relative-path file writes land in the right directory after the
    -# terminal environment is cleaned up and rebuilt (root cause of #26211).
    -_last_known_cwd: dict = {}
    -
    -
    -def _remember_last_known_cwd(task_id: str, cwd: str | None) -> None:
    -    """Mirror a live terminal cwd into the durable ``_last_known_cwd`` registry.
    -
    -    Belt-and-suspenders for #26211: the cleanup thread can pop BOTH
    -    ``_file_ops_cache`` and ``_active_environments`` before ``_get_file_ops``
    -    reaches its stale-cache detection branch, in which case the old cwd is
    -    never saved and the rebuilt env falls back to the config default — exactly
    -    the silent-misplacement bug. By recording the cwd on every successful live
    -    read (which happens on every relative-path file resolution while the env is
    -    alive), the durable anchor no longer depends on the cleanup-detection
    -    branch firing, so it survives recreation regardless of pop ordering.
    -    """
    -    if not cwd:
    -        return
    -    with _file_ops_lock:
    -        if _last_known_cwd.get(task_id) != cwd:
    -            _last_known_cwd[task_id] = cwd
    -
    -
    -def _last_known_cwd_for(task_id: str = "default") -> str | None:
    -    """Read the durable last-known cwd for *task_id*, container-key aware.
    -
    -    The registry is keyed by the resolved container id (the same key used by
    -    the save sites in ``_get_file_ops`` / ``_get_live_tracking_cwd``), so look
    -    up the resolved key first and fall back to the raw task id.
    -    """
    -    try:
    -        from tools.terminal_tool import _resolve_container_task_id
    -        container_key = _resolve_container_task_id(task_id)
    -    except Exception:
    -        container_key = task_id
    -    with _file_ops_lock:
    -        return _last_known_cwd.get(container_key) or _last_known_cwd.get(task_id)
     
     # Track files read per task to detect re-read loops and deduplicate reads.
     # Per task_id we store:
    ```

    ```diff
    @@ -1069,13 +964,18 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
                     _last_activity[task_id] = time.time()
                     return cached
                 else:
    -                # Environment was cleaned up -- preserve the old cwd before
    -                # invalidating the stale cache entry (fixes #26211: silent
    -                # file-creation failures in long-running conversations).
    +                # Environment was cleaned up -- preserve the old cwd in the
    +                # session record before invalidating the stale cache entry
    +                # (fixes #26211: silent file-creation failures in long-running
    +                # conversations). Usually a no-op: every completed command
    +                # already recorded its cwd.
                     old_cwd = getattr(cached, "cwd", None)
                     if old_cwd:
    -                    with _file_ops_lock:
    -                        _last_known_cwd[task_id] = old_cwd
    +                    try:
    +                        from tools.terminal_tool import record_session_cwd
    +                        record_session_cwd(raw_task_id, old_cwd)
    +                    except Exception:
    +                        pass
                     with _file_ops_lock:
                         _file_ops_cache.pop(task_id, None)
     
    ```

    ```diff
    @@ -1113,7 +1013,12 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
                 else:
                     image = ""
     
    -            cwd = overrides.get("cwd") or _last_known_cwd.get(task_id) or config["cwd"]
    +            try:
    +                from tools.terminal_tool import get_session_cwd
    +                recorded_cwd = get_session_cwd(raw_task_id)
    +            except Exception:
    +                recorded_cwd = None
    +            cwd = overrides.get("cwd") or recorded_cwd or config["cwd"]
                 # Re-apply the container cwd guard that _get_env_config() already
                 # ran on config["cwd"] (see #50636).  A per-task cwd override
                 # registered by the gateway/TUI/ACP for workspace tracking is a
    ```
  - `tools/terminal_tool.py`

    ```diff
    @@ -1069,6 +1069,58 @@ def _maybe_reap_docker_orphans(container_config: Dict[str, Any]) -> None:
     # Thread-safe because each task_id is unique per rollout.
     _task_env_overrides: Dict[str, Dict[str, Any]] = {}
     
    +# ── Per-session cwd records (cwd rearchitecture, step 1) ────────────────────
    +#
    +# The durable source of truth for "which directory is THIS session working
    +# in". Keyed by the raw session/task key (NOT the collapsed container id):
    +# the terminal env is shared across sessions, so any cwd state stored on the
    +# env is a global mutable timeshared between sessions — the root cause of the
    +# wrong-worktree bug class (env.cwd_owner stamping, _last_known_cwd, and the
    +# ownership ladder in file_tools are all patches over that misplacement).
    +#
    +# Step 1 (this change): dual-write only. Every site that learns a session's
    +# live cwd (post-command tracking, cwd-override registration) also records it
    +# here. Readers still use the legacy env.cwd ladder. Later steps flip
    +# file_tools and _resolve_command_cwd to read this store, then delete the
    +# env-side tracking + ownership guards.
    +_session_cwd: Dict[str, str] = {}
    +_session_cwd_lock = threading.Lock()
    +
    +
    +def record_session_cwd(session_key: Optional[str], cwd: Optional[str]) -> None:
    +    """Record *cwd* as the working directory of *session_key*.
    +
    +    Called wherever a session's live cwd becomes known: after a terminal
    +    command completes (the env's post-command tracking has just parsed the
    +    resulting cwd) and when a surface registers a workspace cwd override.
    +    Empty/None session keys collapse to ``"default"`` (single-session CLI).
    +    Non-string / empty cwds are ignored.
    +    """
    +    if not isinstance(cwd, str) or not cwd.strip():
    +        return
    +    key = str(session_key or "default")
    +    with _session_cwd_lock:
    +        if _session_cwd.get(key) != cwd:
    +            _session_cwd[key] = cwd
    +
    +
    +def get_session_cwd(session_key: Optional[str]) -> Optional[str]:
    +    """Return the recorded working directory for *session_key*, if any.
    +
    +    No fallback chain here on purpose: callers decide what an absent record
    +    means (config default, TERMINAL_CWD seed, process cwd). ``None``/empty
    +    keys read the ``"default"`` record.
    +    """
    +    key = str(session_key or "default")
    +    with _session_cwd_lock:
    +        return _session_cwd.get(key)
    +
    +
    +def clear_session_cwd(session_key: str) -> None:
    +    """Drop a session's cwd record (session teardown)."""
    +    with _session_cwd_lock:
    +        _session_cwd.pop(session_key, None)
    +
     
     def register_task_env_overrides(task_id: str, overrides: Dict[str, Any]):
         """
    ```

    ```diff
    @@ -1090,15 +1142,14 @@ def register_task_env_overrides(task_id: str, overrides: Dict[str, Any]):
     
         # If a live environment already exists for this task, a freshly registered
         # ``cwd`` override (e.g. the ACP client switching the editor's project root
    -    # mid-session via ``session/load`` / ``session/resume``) must take effect on
    -    # the cached env too. ``terminal_tool`` resolves the per-command cwd as
    -    # ``workdir > env.cwd > config/override cwd`` so that ordinary in-session
    -    # ``cd`` state is preserved; without syncing here the override would sit
    -    # below the (already-set) ``env.cwd`` and be silently ignored once any
    -    # command has run. Pushing it onto the live env keeps ``cd`` tracking intact
    -    # while letting an explicit ACP cwd change win, as the client expects.
    +    # mid-session via ``session/load`` / ``session/resume``) must take effect
    +    # immediately. The session record is what commands resolve against;
    +    # the live env's cwd is also updated so env-side seeding stays consistent.
         new_cwd = overrides.get("cwd")
         if isinstance(new_cwd, str) and new_cwd.strip():
    +        # A registered workspace cwd IS the session's working directory until
    +        # a `cd` changes it.
    +        record_session_cwd(task_id, new_cwd)
             # The live env is cached under the raw task_id for per-session surfaces
             # (ACP/gateway/dashboard) and under the collapsed container id for
             # isolation-keyed rollouts. Try the raw id first, then the container id,
    ```

    ```diff
    @@ -1118,6 +1169,7 @@ def clear_task_env_overrides(task_id: str):
         Called during cleanup to avoid stale entries accumulating.
         """
         _task_env_overrides.pop(task_id, None)
    +    clear_session_cwd(task_id)
     
     
     def _resolve_container_task_id(task_id: Optional[str]) -> str:
    ```

    ```diff
    @@ -1986,43 +2038,21 @@ def _resolve_notification_flag_conflict(
     def _resolve_command_cwd(
         *,
         workdir: Optional[str],
    -    env: Any,
         default_cwd: str,
    -    prev_owner: Optional[str] = None,
    +    session_key: Optional[str] = None,
     ) -> str:
    -    """Return the cwd for a command, preferring the live session cwd.
    -
    -    ``terminal_tool`` historically re-sent the init-time/config cwd on every
    -    call. That broke session-local ``cd`` state: the environment tracked the
    -    new directory in ``env.cwd``, but foreground/background calls kept forcing
    -    the old cwd back through ``env.execute(..., cwd=...)``. Explicit
    -    ``workdir=`` must still override everything.
    -
    -    When ``prev_owner`` is provided and differs from the current session,
    -    ``env.cwd`` was mutated by a *different* session's ``cd`` and must NOT be
    -    trusted — fall through to ``default_cwd`` (the config/override cwd) so
    -    the command runs in this session's own workspace, not the previous
    -    session's leftover checkout.  This mirrors the ``_live_cwd_if_owned``
    -    guard file_tools uses for the same shared-env problem.
    +    """Return the cwd for a command. Explicit ``workdir=`` overrides everything.
    +
    +    Otherwise the session's own cwd RECORD (``get_session_cwd``) wins — it is
    +    written after every completed command for this session, so it IS the
    +    session's ``cd`` state, with no shared-env ambiguity: another session's
    +    ``cd`` lands in another record and can't affect us. A session with no
    +    record yet (first command) runs in ``default_cwd`` (config/override cwd),
    +    which is also what seeds a fresh environment.
         """
         if workdir:
             return workdir
    -
    -    live_cwd = getattr(env, "cwd", None)
    -    if isinstance(live_cwd, str) and live_cwd.strip():
    -        # The env is shared (collapsed to "default"); its cwd tracks the LAST
    -        # session that ran a command.  If a different session owned the env
    -        # before this call claimed it, env.cwd is that session's leftover `cd`
    -        # — not ours.  Don't use it.
    -        if prev_owner is not None:
    -            session_key = getattr(env, "cwd_owner", "")
    -            # cwd_owner was already overwritten to the current session at the
    -            # call site, so compare against the captured previous owner.
    -            if prev_owner and prev_owner != "default" and session_key != prev_owner:
    -                return default_cwd
    -        return live_cwd
    -
    -    return default_cwd
    +    return get_session_cwd(session_key) or default_cwd
     
     
     def terminal_tool(
    ```

    ```diff
    @@ -2111,7 +2141,7 @@ def terminal_tool(
             else:
                 image = ""
     
    -        cwd = overrides.get("cwd") or config["cwd"]
    +        cwd = overrides.get("cwd") or get_session_cwd(task_id) or config["cwd"]
             # A per-task cwd override (registered by the gateway/TUI for workspace
             # tracking, or by RL/benchmark envs) wins over config["cwd"] — but
             # config["cwd"] was already sanitized for container backends in
    ```

    ```diff
    @@ -2164,6 +2194,7 @@ def terminal_tool(
             # Use a per-task creation lock so concurrent tool calls for the same
             # task_id wait for the first one to finish creating the sandbox,
             # instead of each creating their own (wasting Modal resources).
    +        env = None
             with _env_lock:
                 # Prefer the collapsed container id, but fall back to an env cached
                 # under the raw task_id. Per-session surfaces (ACP/gateway/dashboard)
    ```

    ```diff
    @@ -2265,6 +2296,16 @@ def terminal_tool(
                             env = new_env
                         logger.info("%s environment ready for task %s", env_type, effective_task_id[:8])
     
    +        if env is None:
    +            # Unreachable in practice (either the cached branch or the creation
    +            # branch assigned env above); guard for type-safety and so a future
    +            # refactor of the branches can't fall through to an AttributeError.
    +            return json.dumps({
    +                "output": "",
    +                "exit_code": -1,
    +                "error": "Terminal environment unavailable (creation raced cleanup)",
    +            }, ensure_ascii=False)
    +
             # Hard-block: gateway lifecycle commands (systemctl/launchctl/hermes
             # restart|stop targeting hermes-gateway) must never run inside the
             # gateway process itself. The restart would SIGTERM the gateway, which
    ```

    ```diff
    @@ -2362,24 +2403,13 @@ def terminal_tool(
                     "EOF."
                 )
     
    -        # Claim the (shared "default") terminal env for the session driving this
    -        # command. File tools read env.cwd_owner to decide whether the env's live
    -        # cwd is THIS session's `cd` or a different worktree session's — without
    -        # it, two open worktree sessions sharing the env route each other's edits
    -        # to the wrong checkout. get_current_session_key()'s contextvar doesn't
    -        # cross tool-worker threads, so fall back to the raw task_id (which IS the
    -        # session_key for the top-level agent) — a stable, thread-safe anchor.
    +        # The session key that drives cwd records: get_current_session_key()'s
    +        # contextvar doesn't cross tool-worker threads, so fall back to the raw
    +        # task_id (which IS the session_key for the top-level agent) — a
    +        # stable, thread-safe anchor.
             from tools.approval import get_current_session_key
     
             session_key = get_current_session_key(default="") or (task_id or "")
    -        # Capture the env's previous owner BEFORE claiming it — _resolve_command_cwd
    -        # needs to know whether env.cwd was left by a *different* session's `cd`
    -        # (in which case it's stale for this session and must be ignored).
    -        prev_cwd_owner = getattr(env, "cwd_owner", "") or ""
    -        try:
    -            env.cwd_owner = session_key
    -        except Exception:
    -            pass
     
             if background:
                 # Spawn a tracked background process via the process registry.
    ```

    ```diff
    @@ -2389,9 +2419,8 @@ def terminal_tool(
     
                 effective_cwd = _resolve_command_cwd(
                     workdir=workdir,
    -                env=env,
                     default_cwd=cwd,
    -                prev_owner=prev_cwd_owner,
    +                session_key=session_key,
                 )
                 try:
                     if env_type == "local":
    ```

    ```diff
    @@ -2650,9 +2679,8 @@ def terminal_tool(
                     try:
                         command_cwd = _resolve_command_cwd(
                             workdir=workdir,
    -                        env=env,
                             default_cwd=cwd,
    -                        prev_owner=prev_cwd_owner,
    +                        session_key=session_key,
                         )
                         execute_kwargs = {
                             "timeout": effective_timeout,
    ```

    ```diff
    @@ -2693,7 +2721,15 @@ def terminal_tool(
                     
                     # Got a result
                     break
    -            
    +
    +            # Dual-write (cwd rearch step 1): the env's post-command tracking
    +            # (marker parse / local sync) has just updated env.cwd with the
    +            # directory this command finished in. That cwd belongs to THIS
    +            # session — record it under the session key so the durable record
    +            # never depends on the shared env surviving or on who drives the
    +            # env next.
    +            record_session_cwd(session_key, getattr(env, "cwd", None))
    +
                 # Extract output
                 output = result.get("output", "")
                 returncode = result.get("returncode", 0)
    ```
- **#65487: fix(execute-code): resolve project-mode cwd from the session's own directory**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_code_execution_modes.py`, `tools/code_execution_tool.py`
  - fix hunks:
  - `tests/tools/test_code_execution_modes.py`

    ```diff
    @@ -220,6 +220,69 @@ def test_project_expands_tilde(self):
             with patch.dict(os.environ, {"TERMINAL_CWD": "~"}):
                 self.assertEqual(_resolve_child_cwd("project", "/tmp/staging"), home)
     
    +    def test_project_prefers_registered_task_cwd_override(self):
    +        import tempfile
    +        import tools.terminal_tool as terminal_tool
    +
    +        with tempfile.TemporaryDirectory() as td:
    +            task_id = "session-cwd-test"
    +            with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}):
    +                with patch.object(terminal_tool, "_task_env_overrides", {}, create=False):
    +                    terminal_tool.register_task_env_overrides(task_id, {"cwd": td})
    +                    self.assertEqual(_resolve_child_cwd("project", "/tmp/staging", task_id=task_id), td)
    +
    +    def test_project_prefers_session_cwd_record_over_override(self):
    +        """The session's cwd RECORD (its live `cd` state) outranks the
    +        registration-time workspace override — same ladder as file tools
    +        and the terminal, so a `cd` before execute_code is honored."""
    +        import tempfile
    +        import tools.terminal_tool as terminal_tool
    +
    +        with tempfile.TemporaryDirectory() as reg, tempfile.TemporaryDirectory() as cded:
    +            task_id = "session-record-test"
    +            with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}):
    +                with patch.object(terminal_tool, "_task_env_overrides", {}, create=False), \
    +                     patch.object(terminal_tool, "_session_cwd", {}, create=False):
    +                    terminal_tool.register_task_env_overrides(task_id, {"cwd": reg})
    +                    # Simulate a later `cd`: post-command tracking rewrites the record.
    +                    terminal_tool.record_session_cwd(task_id, cded)
    +                    self.assertEqual(
    +                        _resolve_child_cwd("project", "/tmp/staging", task_id=task_id), cded
    +                    )
    +
    +    def test_project_uses_session_cwd_record_without_any_override(self):
    +        """A session that only `cd`'d (no session.cwd.set registration) still
    +        resolves to its recorded directory."""
    +        import tempfile
    +        import tools.terminal_tool as terminal_tool
    +
    +        with tempfile.TemporaryDirectory() as cded:
    +            task_id = "record-only-test"
    +            with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}):
    +                with patch.object(terminal_tool, "_task_env_overrides", {}, create=False), \
    +                     patch.object(terminal_tool, "_session_cwd", {}, create=False):
    +                    terminal_tool.record_session_cwd(task_id, cded)
    +                    self.assertEqual(
    +                        _resolve_child_cwd("project", "/tmp/staging", task_id=task_id), cded
    +                    )
    +
    +    def test_project_stale_record_falls_through_to_override(self):
    +        """A recorded directory that no longer exists is skipped; the
    +        registered override is the next rung."""
    +        import tempfile
    +        import tools.terminal_tool as terminal_tool
    +
    +        with tempfile.TemporaryDirectory() as reg:
    +            task_id = "stale-record-test"
    +            with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}):
    +                with patch.object(terminal_tool, "_task_env_overrides", {}, create=False), \
    +                     patch.object(terminal_tool, "_session_cwd", {}, create=False):
    +                    terminal_tool.register_task_env_overrides(task_id, {"cwd": reg})
    +                    terminal_tool.record_session_cwd(task_id, "/deleted/dir/gone")
    +                    self.assertEqual(
    +                        _resolve_child_cwd("project", "/tmp/staging", task_id=task_id), reg
    +                    )
    +
     
     # ---------------------------------------------------------------------------
     # Schema description
    ```

    ```diff
    @@ -316,6 +379,28 @@ def test_project_mode_runs_in_session_cwd(self):
                     os.path.realpath(td),
                 )
     
    +    def test_project_mode_uses_registered_session_cwd_override(self):
    +        """Project mode must honor session.cwd.set-style overrides even when
    +        TERMINAL_CWD is absent or points elsewhere."""
    +        import tempfile
    +        import tools.terminal_tool as terminal_tool
    +
    +        with tempfile.TemporaryDirectory() as td:
    +            task_id = "session-cwd-test"
    +            with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}):
    +                with patch.object(terminal_tool, "_task_env_overrides", {}, create=False):
    +                    terminal_tool.register_task_env_overrides(task_id, {"cwd": td})
    +                    with _mock_mode("project"):
    +                        with patch("model_tools.handle_function_call", side_effect=_mock_handle_function_call):
    +                            raw = execute_code(
    +                                code="import os; print(os.getcwd())",
    +                                task_id=task_id,
    +                                enabled_tools=list(SANDBOX_ALLOWED_TOOLS),
    +                            )
    +            result = json.loads(raw)
    +            self.assertEqual(result["status"], "success")
    +            self.assertEqual(os.path.realpath(result["output"].strip()), os.path.realpath(td))
    +
         def test_project_mode_interpreter_is_venv_python(self):
             """Project mode: sys.executable inside the child is the venv's python
             when VIRTUAL_ENV is set to a real venv."""
    ```
  - `tools/code_execution_tool.py`

    ```diff
    @@ -1339,7 +1339,7 @@ def execute_code(
             # Env scrubbing and tool whitelist apply identically in both modes.
             _mode = _get_execution_mode()
             _child_python = _resolve_child_python(_mode)
    -        _child_cwd = _resolve_child_cwd(_mode, tmpdir)
    +        _child_cwd = _resolve_child_cwd(_mode, tmpdir, task_id=task_id or "")
             _script_path = os.path.join(tmpdir, "script.py")
     
             proc = subprocess.Popen(
    ```

    ```diff
    @@ -1745,17 +1745,43 @@ def _resolve_child_python(mode: str) -> str:
         return sys.executable
     
     
    -def _resolve_child_cwd(mode: str, staging_dir: str) -> str:
    +def _resolve_child_cwd(mode: str, staging_dir: str, task_id: str = "") -> str:
         """Resolve the working directory for the execute_code subprocess.
     
         - ``strict``: the staging tmpdir (today's behavior).
    -    - ``project``: the session's TERMINAL_CWD (same as the terminal tool), or
    -      ``os.getcwd()`` if TERMINAL_CWD is unset or doesn't point at a real dir.
    -      Falls back to the staging tmpdir as a last resort so we never invoke
    -      Popen with a nonexistent cwd.
    +    - ``project``: the session's own cwd — its per-session cwd record
    +      (written after every completed terminal command), then the raw
    +      per-session cwd override registered via ``session.cwd.set`` /
    +      ``register_task_env_overrides``, then the session's TERMINAL_CWD
    +      (same as the terminal tool), or ``os.getcwd()`` if none points at a
    +      real dir. Falls back to the staging tmpdir as a last resort so we
    +      never invoke Popen with a nonexistent cwd.
    +
    +    This mirrors the resolution ladder file tools and the terminal use
    +    (record → registered override → TERMINAL_CWD), so all file-writing
    +    paths within a session agree on the working directory. (#56047)
         """
         if mode != "project":
             return staging_dir
    +    if task_id:
    +        # 1. The session's cwd record — IS the session's `cd` state.
    +        try:
    +            from tools.terminal_tool import get_session_cwd
    +
    +            recorded = get_session_cwd(task_id)
    +        except Exception:
    +            recorded = None
    +        if recorded and os.path.isdir(recorded):
    +            return recorded
    +        # 2. Registered workspace override (session.cwd.set → gateway/TUI/ACP).
    +        try:
    +            from tools.file_tools import _registered_task_cwd_override
    +
    +            session_cwd = _registered_task_cwd_override(task_id)
    +        except Exception:
    +            session_cwd = None
    +        if session_cwd and os.path.isdir(session_cwd):
    +            return session_cwd
         raw = os.environ.get("TERMINAL_CWD", "").strip()
         if raw:
             expanded = os.path.expanduser(raw)
    ```
- **#35767: fix(gateway): show live cwd in runtime footer**. open
  - maintainer direction: category `silent_close`; implication `active`
  - files: `gateway/run.py`, `gateway/runtime_footer.py`, `tests/gateway/test_runtime_footer.py`
- **#36895: fix(skills): ignore asset markdown in skill_view collision scan**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `9137b86a5286e4ea420e9fb894bce34fee546b3d`, `#37164`
- **#49658: fix(tui): always capture current CWD instead of inheriting stale HERMES_CWD**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#49662`, `#49662`, `#49662`, `#49637`
- **#56055: fix(agent): resolve execute_code cwd from per-session override (#56047)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `298a94926`, `#65487`, `#56803`, `#65213`
- **#56069: fix(code-exec): honor session cwd override**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#35513`, `#56055`, `#46843`, `#56047`, `#35513`
- **#56803: fix(execute-code): honor session cwd overrides**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `4d6686c18`, `#65487`, `#56055`, `#65213`
- **#104615: fix(gateway): persist per-session workspaces**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#42577`, `#37277`, `#37275`, `#42577`
  - files: `README.md`, `cli-config.yaml.example`, `gateway/config.py`, `gateway/run.py`, `gateway/run_busy.py`, `gateway/run_config_loaders.py`, `gateway/run_turn.py`, `gateway/session.py`, `gateway/session_recovery.py`, `gateway/slash_commands_session.py`, `gateway/workspace.py`, `hermes_cli/commands.py`, `hermes_state_sessions.py`, `locales/en.yaml`, `tests/gateway/test_workspace_routing.py`, `tools/terminal_tool.py`, `website/docs/user-guide/messaging/index.md`
- **#105478: feat(api-server): bind per-run workspace cwd on /v1/runs**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `67481b6f37`, `#78923`, `#109230`, `#136998`, `#29531`
  - files: `gateway/platforms/api_server.py`, `gateway/platforms/api_server_runs.py`, `tests/gateway/test_api_server_runs_workspace.py`
- **#105991: feat(tui): add /cd slash command and scope path completion to session cwd**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `ui-tui/src/__tests__/useCompletion.test.ts`, `ui-tui/src/app/slash/commands/session.ts`, `ui-tui/src/app/useComposerState.ts`, `ui-tui/src/hooks/useCompletion.ts`
- **#109839: fix(tui): make the launch cwd win over an inherited HERMES_CWD**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`

## Fix status

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