{"repo":"NousResearch/hermes-agent","public_id":"38775","display_label":"Working directory handling fixes across session types","summary":"A bug and several fixes addressing how the configured or current working directory is tracked and applied across app-server sessions, terminal sessions, and desktop workspaces. Changes include per-session cwd records, seeding sessions with the correct cwd, and ensuring configured directories take precedence over stale remembered values.","subsystem":"acp","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50451,"merged_at":"2026-06-21T23:39:03+00:00"},"resolution":{"state":"resolved","by":"pr-50451"},"freshness":{"state":"verified","last_verified_at":"2026-06-21T23:39:03+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":38775,"pr_number":50451,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-21T23:39:03+00:00"}],"members":[{"kind":"issue","number":38775,"title":"[Bug]: Codex app-server sessions ignore configured/session cwd and start sandbox in Hermes home","url":null,"state":"closed"},{"kind":"pr","number":38778,"title":"fix(codex): seed app-server sessions with configured cwd","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"c11ae8261","number":0},{"kind":"pr","value":"#50451","number":50451}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":50451,"title":"fix(codex): seed app-server sessions with configured cwd","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["acp_adapter/session.py","agent/codex_runtime.py","tests/acp/test_session.py","tests/run_agent/test_codex_app_server_integration.py"],"fix_hunks":[{"file":"acp_adapter/session.py","hunks":["@@ -617,6 +617,10 @@ def _make_agent(\n \n         _register_task_cwd(session_id, cwd)\n         agent = AIAgent(**kwargs)\n+        # Codex app-server sessions are spawned lazily on the first turn. Stamp\n+        # the ACP workspace onto the agent so the Codex runtime starts from the\n+        # editor/session cwd instead of the Hermes daemon's process cwd.\n+        agent.session_cwd = cwd\n         # ACP stdio transport requires stdout to remain protocol-only JSON-RPC.\n         # Route any incidental human-readable agent output to stderr instead.\n         agent._print_fn = _acp_stderr_print"]},{"file":"agent/codex_runtime.py","hunks":["@@ -250,7 +250,9 @@ def run_codex_app_server_turn(\n     # Spawned on first turn, reused across turns, closed at AIAgent\n     # shutdown (see _cleanup hook).\n     if not hasattr(agent, \"_codex_session\") or agent._codex_session is None:\n-        cwd = getattr(agent, \"session_cwd\", None) or os.getcwd()\n+        from agent.runtime_cwd import resolve_agent_cwd\n+\n+        cwd = getattr(agent, \"session_cwd\", None) or str(resolve_agent_cwd())\n         # Approval callback: defer to Hermes' standard prompt flow if a\n         # CLI thread has installed one. Gateway / cron contexts get the\n         # codex-side fail-closed default."]},{"file":"tests/acp/test_session.py","hunks":["@@ -77,6 +77,50 @@ def test_get_session(self, manager):\n     def test_get_nonexistent_session_returns_none(self, manager):\n         assert manager.get_session(\"does-not-exist\") is None\n \n+    def test_make_agent_stamps_session_cwd_for_codex_runtime(self, monkeypatch):\n+        class FakeAgent:\n+            model = \"fake-model\"\n+\n+            def __init__(self, **kwargs):\n+                self.kwargs = kwargs\n+\n+        monkeypatch.setattr(\"run_agent.AIAgent\", FakeAgent)\n+        monkeypatch.setattr(\n+            \"acp_adapter.session.load_config\",\n+            lambda: {\n+                \"model\": {\n+                    \"default\": \"fake-model\",\n+                    \"provider\": \"fake-provider\",\n+                },\n+                \"mcp_servers\": {},\n+            },\n+            raising=False,\n+        )\n+        monkeypatch.setattr(\n+            \"hermes_cli.config.load_config\",\n+            lambda: {\n+                \"model\": {\n+                    \"default\": \"fake-model\",\n+                    \"provider\": \"fake-provider\",\n+                },\n+                \"mcp_servers\": {},\n+            },\n+        )\n+        monkeypatch.setattr(\n+            \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+            lambda requested=None: {\n+                \"provider\": requested,\n+                \"api_mode\": \"codex_app_server\",\n+                \"base_url\": \"https://example.invalid\",\n+                \"api_key\": \"[redacted:secret-assignment]\",\n+            },\n+        )\n+        monkeypatch.setattr(\"acp_adapter.session._register_task_cwd\", lambda task_id, cwd: None)\n+\n+        state = SessionManager(db=None).create_session(cwd=\"/tmp/project\")\n+\n+        assert state.agent.session_cwd == \"/tmp/project\"\n+\n \n \n "]},{"file":"tests/run_agent/test_codex_app_server_integration.py","hunks":["@@ -293,6 +293,39 @@ def test_chat_completions_loop_is_not_entered(self, fake_session):\n             agent.run_conversation(\"hi\")\n         assert not client_mock.chat.completions.create.called\n \n+    def test_gateway_terminal_cwd_seeds_codex_thread_cwd(self, monkeypatch, tmp_path):\n+        \"\"\"Gateway sessions set TERMINAL_CWD without stamping agent.session_cwd.\n+        Codex app-server must still start in that configured workspace instead\n+        of falling back to the Hermes daemon process cwd.\"\"\"\n+        from agent.transports.codex_app_server_session import (\n+            CodexAppServerSession, TurnResult,\n+        )\n+\n+        captured: dict[str, str] = {}\n+\n+        def fake_init(self, **kwargs):\n+            captured[\"cwd\"] = kwargs[\"cwd\"]\n+            self._thread_id = \"thread-stub-1\"\n+\n+        def fake_run_turn(self, user_input: str, **kwargs):\n+            return TurnResult(\n+                final_text=\"ok\",\n+                projected_messages=[{\"role\": \"assistant\", \"content\": \"ok\"}],\n+                turn_id=\"turn-stub-1\",\n+                thread_id=\"thread-stub-1\",\n+            )\n+\n+        monkeypatch.setenv(\"TERMINAL_CWD\", str(tmp_path))\n+        monkeypatch.setattr(CodexAppServerSession, \"__init__\", fake_init)\n+        monkeypatch.setattr(CodexAppServerSession, \"run_turn\", fake_run_turn)\n+\n+        agent = _make_codex_agent()\n+        assert not hasattr(agent, \"session_cwd\")\n+        with patch.object(agent, \"_spawn_background_review\", return_value=None):\n+            agent.run_conversation(\"hi\")\n+\n+        assert captured[\"cwd\"] == str(tmp_path)\n+\n \n class TestReviewForkApiModeDowngrade:\n     \"\"\"When the parent agent runs on codex_app_server, the background\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}