NousResearch/hermes-agent

Kanban worker TERMINAL_CWD path resolution bug and fixes

Cluster 41312 · environments. Data as of

Nothing open on GitHub. Fix merged in #50348 on 21 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #50348. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

A bug where Kanban workers resolve relative file paths against the wrong working directory (gateway home instead of the assigned workspace) and several related fix attempts targeting TERMINAL_CWD environment variable handling.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #50348 fix(kanban): pin worker TERMINAL_CWD to the task workspace

    merged to main

    files: hermes_cli/kanban_db.py, tests/hermes_cli/test_kanban_worker_terminal_cwd.py

    Show the diff
    • hermes_cli/kanban_db.py
      @@ -7246,6 +7246,20 @@ def _default_spawn(
               env["HERMES_TENANT"] = task.tenant
           env["HERMES_KANBAN_TASK"] = task.id
           env["HERMES_KANBAN_WORKSPACE"] = workspace
      +    # Pin TERMINAL_CWD to the task's workspace so the worker's file tools and
      +    # context-file loader anchor on the workspace, not whatever cwd the
      +    # dispatching gateway happened to export. The worker subprocess is already
      +    # launched with cwd=workspace, but TERMINAL_CWD takes precedence over the
      +    # process cwd in both file_tools._resolve_base_dir (#41312 — relative
      +    # write_file paths were landing in the gateway user's home) and
      +    # build_context_files_prompt (#34619 — workers loaded the dispatching
      +    # gateway's AGENTS.md instead of the task's). Setting it to the workspace
      +    # fixes both: the workspace is where the task's work actually happens.
      +    # Only pin a real, absolute directory — file_tools rejects relative /
      +    # sentinel TERMINAL_CWD values, so a non-dir workspace must NOT be set
      +    # here (leave the inherited value rather than write a meaningless one).
      +    if workspace and os.path.isabs(workspace) and os.path.isdir(workspace):
      +        env["TERMINAL_CWD"] = workspace
           if task.branch_name:
               env["HERMES_KANBAN_BRANCH"] = task.branch_name
           if task.current_run_id is not None:
    • tests/hermes_cli/test_kanban_worker_terminal_cwd.py
      @@ -0,0 +1,101 @@
      +"""Tests: kanban worker spawn pins TERMINAL_CWD to the task workspace.
      +
      +Regression coverage for #34619 and #41312 (same root cause): ``_default_spawn``
      +launched the worker subprocess with ``cwd=workspace`` and set
      +``HERMES_KANBAN_WORKSPACE``, but did NOT set ``TERMINAL_CWD``. Because
      +``TERMINAL_CWD`` takes precedence over the process cwd in both
      +``tools/file_tools.py::_resolve_base_dir`` (relative ``write_file`` paths) and
      +``agent_init``'s context-file loader (``AGENTS.md`` discovery), workers inherited
      +the dispatching gateway's cwd — relative writes landed in the gateway user's
      +home (#41312) and the wrong profile's ``AGENTS.md`` was loaded (#34619).
      +Pinning ``TERMINAL_CWD`` to the workspace fixes both.
      +"""
      +
      +from __future__ import annotations
      +
      +import subprocess
      +
      +
      +def _make_task(kb, *, assignee: str = "w"):
      +    return kb.Task(
      +        id="t_cwd",
      +        title="cwd pin",
      +        body=None,
      +        assignee=assignee,
      +        status="running",
      +        priority=0,
      +        created_by="test",
      +        created_at=1,
      +        started_at=None,
      +        completed_at=None,
      +        workspace_kind="dir",
      +        workspace_path=None,
      +        claim_lock="lock",
      +        claim_expires=None,
      +        tenant=None,
      +        current_run_id=1,
      +    )
      +
      +
      +def _capture_spawn_env(kb, monkeypatch, workspace: str) -> dict:
      +    monkeypatch.setattr(kb, "_resolve_hermes_argv", lambda: ["hermes"])
      +
      +    captured: dict = {}
      +
      +    class FakeProc:
      +        pid = 4242
      +
      +    def fake_popen(cmd, *args, **kwargs):
      +        captured["cmd"] = list(cmd)
      +        captured["env"] = dict(kwargs.get("env") or {})
      +        captured["cwd"] = kwargs.get("cwd")
      +        return FakeProc()
      +
      +    monkeypatch.setattr(subprocess, "Popen", fake_popen)
      +    kb._default_spawn(_make_task(kb), workspace)
      +    return captured
      +
      +
      +def test_terminal_cwd_pinned_to_workspace(monkeypatch, tmp_path):
      +    """A real, absolute workspace dir is pinned as TERMINAL_CWD."""
      +    root = tmp_path / ".hermes"
      +    (root / "profiles" / "w").mkdir(parents=True)
      +    (root / "profiles" / "w" / "config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    root.joinpath("config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    monkeypatch.setenv("HERMES_HOME", str(root))
      +
      +    from hermes_cli import kanban_db as kb
      +
      +    workspace = tmp_path / "ws"
      +    workspace.mkdir()
      +
      +    captured = _capture_spawn_env(kb, monkeypatch, str(workspace))
      +
      +    assert captured["env"]["TERMINAL_CWD"] == str(workspace)
      +    # The subprocess cwd and TERMINAL_CWD must agree — both anchor the workspace.
      +    assert captured["cwd"] == str(workspace)
      +    assert captured["env"]["HERMES_KANBAN_WORKSPACE"] == str(workspace)
      +
      +
      +def test_terminal_cwd_not_pinned_for_nonexistent_workspace(monkeypatch, tmp_path):
      +    """A non-directory workspace must NOT clobber the inherited TERMINAL_CWD.
      +
      +    file_tools rejects relative / sentinel TERMINAL_CWD values, so writing a
      +    meaningless (nonexistent) path would be worse than leaving the inherited
      +    one. The guard requires an existing absolute dir.
      +    """
      +    root = tmp_path / ".hermes"
      +    (root / "profiles" / "w").mkdir(parents=True)
      +    (root / "profiles" / "w" / "config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    root.joinpath("config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    monkeypatch.setenv("HERMES_HOME", str(root))
      +    monkeypatch.setenv("TERMINAL_CWD", "/pre/existing/anchor")
      +
      +    from hermes_cli import kanban_db as kb
      +
      +    missing = tmp_path / "does-not-exist"
      +
      +    captured = _capture_spawn_env(kb, monkeypatch, str(missing))
      +
      +    # Inherited value is preserved (not overwritten with a bogus path).
      +    assert captured["env"]["TERMINAL_CWD"] == "/pre/existing/anchor"

2 PRs closed without merging

From the close comments: done elsewhere 1 · other 1

  • #41436 fix(kanban): pin TERMINAL_CWD to workspace so relative file paths resolve correctly · done elsewhere → 9630ec6, #50348, #41312
  • #41450 fix(kanban): set TERMINAL_CWD in worker env for correct file path resolution (#41312) · other
Closed issues · 1
  • Closed#41312[Bug]: Kanban worker resolves relative file paths against TERMINAL_CWD (gateway home), not the assigned HERMES_KANBAN_WORKSPACE
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
environments
Merged PRs
#50348
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
21 Jun 2026

For your agent

The same facts, typed.