{"repo":"NousResearch/hermes-agent","public_id":"73556","display_label":"Kanban task workspace vs profile terminal.cwd priority","summary":"These issues and fixes address a conflict where a profile's terminal.cwd setting could override the kanban task workspace, with security implications for Docker mount scope. The changes make the task workspace authoritative over profile terminal.cwd.","subsystem":"hermes_cli","counts":{"members":3,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":0,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50348,"merged_at":"2026-06-21T19:43:37+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-21T19:43:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":34619,"pr_number":50348,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-21T19:43:37+00:00"},{"issue_number":41312,"pr_number":50348,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-21T19:43:37+00:00"}],"members":[{"kind":"issue","number":73556,"title":"[Security][Kanban] profile terminal.cwd overrides task workspace and broadens Docker mount","url":null,"state":"open"},{"kind":"pr","number":50348,"title":"fix(kanban): pin worker TERMINAL_CWD to the task workspace","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/kanban_db.py","tests/hermes_cli/test_kanban_worker_terminal_cwd.py"],"fix_hunks":[{"file":"hermes_cli/kanban_db.py","hunks":["@@ -7246,6 +7246,20 @@ def _default_spawn(\n         env[\"HERMES_TENANT\"] = task.tenant\n     env[\"HERMES_KANBAN_TASK\"] = task.id\n     env[\"HERMES_KANBAN_WORKSPACE\"] = workspace\n+    # Pin TERMINAL_CWD to the task's workspace so the worker's file tools and\n+    # context-file loader anchor on the workspace, not whatever cwd the\n+    # dispatching gateway happened to export. The worker subprocess is already\n+    # launched with cwd=workspace, but TERMINAL_CWD takes precedence over the\n+    # process cwd in both file_tools._resolve_base_dir (#41312 — relative\n+    # write_file paths were landing in the gateway user's home) and\n+    # build_context_files_prompt (#34619 — workers loaded the dispatching\n+    # gateway's AGENTS.md instead of the task's). Setting it to the workspace\n+    # fixes both: the workspace is where the task's work actually happens.\n+    # Only pin a real, absolute directory — file_tools rejects relative /\n+    # sentinel TERMINAL_CWD values, so a non-dir workspace must NOT be set\n+    # here (leave the inherited value rather than write a meaningless one).\n+    if workspace and os.path.isabs(workspace) and os.path.isdir(workspace):\n+        env[\"TERMINAL_CWD\"] = workspace\n     if task.branch_name:\n         env[\"HERMES_KANBAN_BRANCH\"] = task.branch_name\n     if task.current_run_id is not None:"]},{"file":"tests/hermes_cli/test_kanban_worker_terminal_cwd.py","hunks":["@@ -0,0 +1,101 @@\n+\"\"\"Tests: kanban worker spawn pins TERMINAL_CWD to the task workspace.\n+\n+Regression coverage for #34619 and #41312 (same root cause): ``_default_spawn``\n+launched the worker subprocess with ``cwd=workspace`` and set\n+``HERMES_KANBAN_WORKSPACE``, but did NOT set ``TERMINAL_CWD``. Because\n+``TERMINAL_CWD`` takes precedence over the process cwd in both\n+``tools/file_tools.py::_resolve_base_dir`` (relative ``write_file`` paths) and\n+``agent_init``'s context-file loader (``AGENTS.md`` discovery), workers inherited\n+the dispatching gateway's cwd — relative writes landed in the gateway user's\n+home (#41312) and the wrong profile's ``AGENTS.md`` was loaded (#34619).\n+Pinning ``TERMINAL_CWD`` to the workspace fixes both.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import subprocess\n+\n+\n+def _make_task(kb, *, assignee: str = \"w\"):\n+    return kb.Task(\n+        id=\"t_cwd\",\n+        title=\"cwd pin\",\n+        body=None,\n+        assignee=assignee,\n+        status=\"running\",\n+        priority=0,\n+        created_by=\"test\",\n+        created_at=1,\n+        started_at=None,\n+        completed_at=None,\n+        workspace_kind=\"dir\",\n+        workspace_path=None,\n+        claim_lock=\"lock\",\n+        claim_expires=None,\n+        tenant=None,\n+        current_run_id=1,\n+    )\n+\n+\n+def _capture_spawn_env(kb, monkeypatch, workspace: str) -> dict:\n+    monkeypatch.setattr(kb, \"_resolve_hermes_argv\", lambda: [\"hermes\"])\n+\n+    captured: dict = {}\n+\n+    class FakeProc:\n+        pid = 4242\n+\n+    def fake_popen(cmd, *args, **kwargs):\n+        captured[\"cmd\"] = list(cmd)\n+        captured[\"env\"] = dict(kwargs.get(\"env\") or {})\n+        captured[\"cwd\"] = kwargs.get(\"cwd\")\n+        return FakeProc()\n+\n+    monkeypatch.setattr(subprocess, \"Popen\", fake_popen)\n+    kb._default_spawn(_make_task(kb), workspace)\n+    return captured\n+\n+\n+def test_terminal_cwd_pinned_to_workspace(monkeypatch, tmp_path):\n+    \"\"\"A real, absolute workspace dir is pinned as TERMINAL_CWD.\"\"\"\n+    root = tmp_path / \".hermes\"\n+    (root / \"profiles\" / \"w\").mkdir(parents=True)\n+    (root / \"profiles\" / \"w\" / \"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    root.joinpath(\"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    monkeypatch.setenv(\"HERMES_HOME\", str(root))\n+\n+    from hermes_cli import kanban_db as kb\n+\n+    workspace = tmp_path / \"ws\"\n+    workspace.mkdir()\n+\n+    captured = _capture_spawn_env(kb, monkeypatch, str(workspace))\n+\n+    assert captured[\"env\"][\"TERMINAL_CWD\"] == str(workspace)\n+    # The subprocess cwd and TERMINAL_CWD must agree — both anchor the workspace.\n+    assert captured[\"cwd\"] == str(workspace)\n+    assert captured[\"env\"][\"HERMES_KANBAN_WORKSPACE\"] == str(workspace)\n+\n+\n+def test_terminal_cwd_not_pinned_for_nonexistent_workspace(monkeypatch, tmp_path):\n+    \"\"\"A non-directory workspace must NOT clobber the inherited TERMINAL_CWD.\n+\n+    file_tools rejects relative / sentinel TERMINAL_CWD values, so writing a\n+    meaningless (nonexistent) path would be worse than leaving the inherited\n+    one. The guard requires an existing absolute dir.\n+    \"\"\"\n+    root = tmp_path / \".hermes\"\n+    (root / \"profiles\" / \"w\").mkdir(parents=True)\n+    (root / \"profiles\" / \"w\" / \"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    root.joinpath(\"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    monkeypatch.setenv(\"HERMES_HOME\", str(root))\n+    monkeypatch.setenv(\"TERMINAL_CWD\", \"/pre/existing/anchor\")\n+\n+    from hermes_cli import kanban_db as kb\n+\n+    missing = tmp_path / \"does-not-exist\"\n+\n+    captured = _capture_spawn_env(kb, monkeypatch, str(missing))\n+\n+    # Inherited value is preserved (not overwritten with a bogus path).\n+    assert captured[\"env\"][\"TERMINAL_CWD\"] == \"/pre/existing/anchor\"\n"]}]},{"kind":"pr","number":73707,"title":"fix(cli): keep the kanban task workspace authoritative over profile terminal.cwd","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"9630ec6c19e6b060ad16e5cc6ae00c4f3ecba776","number":0},{"kind":"commit","value":"7dbd6a9bb04d","number":0},{"kind":"pr","value":"#30783","number":30783},{"kind":"pr","value":"#34626","number":34626},{"kind":"pr","value":"#41436","number":41436},{"kind":"pr","value":"#41450","number":41450},{"kind":"pr","value":"#50348","number":50348},{"kind":"pr","value":"#73707","number":73707},{"kind":"pr","value":"#73556","number":73556},{"kind":"pr","value":"#34619","number":34619},{"kind":"pr","value":"#41312","number":41312},{"kind":"pr","value":"#374151","number":374151}],"category":"partial_fix","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}