NousResearch/hermes-agent

Profile switch CWD and terminal.cwd priority fixes

Cluster 40334 · apps/desktop. Data as of

3 open items. Fix merged in #122310 on 25 Sep 2026. Check it covers your case.

The short answer

0open issues
3open PRs
9items grouped
Known?
Yes. 9 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #122310. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #40892, #122310. That is separate from the fix event.
Still open
0 issues and 3 PRs.

A cluster of bugs and fixes around the Desktop app failing to apply the correct working directory when switching profiles, specifically that terminal.cwd from the target profile was being ignored in favor of stale workspace or previous-profile paths.

Open issues0

No open issues.

Open PRs3

  • PR#56869fix(tui_gateway): prefer profile terminal.cwd over stale client-sent cwd on profile switch
  • PR#61653fix: profile terminal.cwd overrides desktop app-global workspace (#52589)
  • PR#63976fix(desktop): clear project scope on profile switch

Fixes on record

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

  • #40892 fix(tui_gateway): honor target profile's terminal.cwd on desktop profile switch

    merged to main

    files: tests/test_tui_gateway_server.py, tui_gateway/server.py

    Show the diff
    • tests/test_tui_gateway_server.py
      @@ -60,6 +60,64 @@ def test_session_context_explicit_cwd_for_ephemeral_task(monkeypatch, tmp_path):
               server._clear_session_context(tokens)
       
       
      +def _write_profile_cfg(home: Path, cwd: str | None) -> Path:
      +    import yaml
      +
      +    home.mkdir(parents=True, exist_ok=True)
      +    cfg = {"terminal": {"cwd": cwd}} if cwd is not None else {}
      +    (home / "config.yaml").write_text(yaml.safe_dump(cfg), encoding="utf-8")
      +    return home
      +
      +
      +def test_profile_configured_cwd_reads_target_profile(tmp_path):
      +    """A profile's own terminal.cwd is read from its config.yaml."""
      +    project = tmp_path / "proj"
      +    project.mkdir()
      +    home = _write_profile_cfg(tmp_path / "home", str(project))
      +    assert server._profile_configured_cwd(home) == str(project)
      +
      +
      +def test_profile_configured_cwd_skips_placeholders_and_missing(tmp_path):
      +    """Placeholder values, missing config, and bad paths fall through to None."""
      +    assert server._profile_configured_cwd(None) is None
      +    assert server._profile_configured_cwd(tmp_path / "nope") is None
      +    for placeholder in (".", "auto", "cwd", ""):
      +        home = _write_profile_cfg(tmp_path / placeholder.strip("."), placeholder)
      +        assert server._profile_configured_cwd(home) is None
      +    home = _write_profile_cfg(tmp_path / "ghost", str(tmp_path / "does-not-exist"))
      +    assert server._profile_configured_cwd(home) is None
      +
      +
      +def test_completion_cwd_prefers_profile_over_stale_env(monkeypatch, tmp_path):
      +    """Issue #40334: a new session bound to another profile must use THAT
      +    profile's terminal.cwd, not the launch profile's stale TERMINAL_CWD."""
      +    profile_b = tmp_path / "ef-design"
      +    profile_b.mkdir()
      +    home = _write_profile_cfg(tmp_path / "home-b", str(profile_b))
      +    stale = tmp_path / "mahjong"
      +    stale.mkdir()
      +
      +    monkeypatch.setenv("TERMINAL_CWD", str(stale))
      +    monkeypatch.setattr(server, "_profile_home", lambda name: home if name else None)
      +
      +    assert server._completion_cwd({"profile": "ef-design"}) == str(profile_b)
      +    # No profile → unchanged fallback to the launch env var.
      +    assert server._completion_cwd({}) == str(stale)
      +
      +
      +def test_completion_cwd_explicit_cwd_wins_over_profile(monkeypatch, tmp_path):
      +    """An explicit client-provided cwd still beats the profile config."""
      +    explicit = tmp_path / "explicit"
      +    explicit.mkdir()
      +    profile_b = tmp_path / "configured"
      +    profile_b.mkdir()
      +    home = _write_profile_cfg(tmp_path / "home-c", str(profile_b))
      +
      +    monkeypatch.setattr(server, "_profile_home", lambda name: home if name else None)
      +    result = server._completion_cwd({"cwd": str(explicit), "profile": "ef-design"})
      +    assert result == str(explicit)
      +
      +
       def test_terminal_task_cwd_local_backend_uses_session_cwd(monkeypatch, tmp_path):
           """A local terminal backend must keep host-validated session cwd behaviour."""
           project = tmp_path / "project"
    • tui_gateway/server.py
      @@ -488,6 +488,40 @@ def _profile_home(profile: str | None) -> Path | None:
           return home if (home / "state.db").exists() or home.exists() else None
       
       
      +# Placeholder ``terminal.cwd`` values that don't name a real directory — the
      +# gateway resolves these to the home dir at runtime, so they must NOT be treated
      +# as an explicit workspace (mirrors gateway/run.py's config bridge).
      +_CWD_PLACEHOLDERS = {".", "auto", "cwd"}
      +
      +
      +def _profile_configured_cwd(profile_home: Path | None) -> str | None:
      +    """Resolve a non-launch profile's ``terminal.cwd`` from its own config.yaml.
      +
      +    The desktop's app-global remote mode serves every profile from one backend,
      +    so the process-global ``TERMINAL_CWD`` belongs to the *launch* profile. A new
      +    session bound to another profile must take its workspace from THAT profile's
      +    config, not the stale env var (issue #40334). Returns an absolute, existing
      +    directory, or None for placeholders / missing / invalid paths.
      +    """
      +    if profile_home is None:
      +        return None
      +    try:
      +        import yaml
      +
      +        p = Path(profile_home) / "config.yaml"
      +        if not p.exists():
      +            return None
      +        with open(p, encoding="utf-8") as f:
      +            data = yaml.safe_load(f) or {}
      +        raw = str((data.get("terminal") or {}).get("cwd") or "").strip()
      +        if not raw or raw in _CWD_PLACEHOLDERS:
      +            return None
      +        resolved = os.path.abspath(os.path.expanduser(raw))
      +        return resolved if os.path.isdir(resolved) else None
      +    except Exception:
      +        return None
      +
      +
       def write_json(obj: dict) -> bool:
           """Emit one JSON frame. Routes via the most-specific transport available.
       
      @@ -798,9 +832,13 @@ def _normalize_completion_path(path_part: str) -> str:
       
       
       def _completion_cwd(params: dict | None = None) -> str:
      +    params = params or {}
           raw = (
      -        (params or {}).get("cwd")
      -        or _sessions.get((params or {}).get("session_id") or "", {}).get("cwd")
      +        params.get("cwd")
      +        or _sessions.get(params.get("session_id") or "", {}).get("cwd")
      +        # A session bound to another profile resolves its workspace from THAT
      +        # profile's config before falling back to the launch profile's env var.
      +        or _profile_configured_cwd(_profile_home(params.get("profile")))
               or os.environ.get("TERMINAL_CWD")
               or os.getcwd()
           )
  • #122310 fix(desktop): leave the entered project when switching profile or backend

    merged to main

    files: apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx, apps/desktop/src/app/chat/sidebar/index.tsx, apps/desktop/src/app/session/hooks/default-new-session.test.tsx, apps/desktop/src/app/session/hooks/use-session-actions.test.tsx, apps/desktop/src/app/session/hooks/use-session-actions/index.ts, apps/desktop/src/app/session/new-session-route.ts, apps/desktop/src/app/session/workspace-session-target.test.ts, apps/desktop/src/store/coding-status.ts, apps/desktop/src/store/gateway-switch.test.ts, apps/desktop/src/store/gateway-switch.ts, apps/desktop/src/store/profile.test.ts, apps/desktop/src/store/profile.ts, apps/desktop/src/store/project-scope.ts, apps/desktop/src/store/projects.test.ts, apps/desktop/src/store/projects.ts

1 PR closed without merging

From the close comments: done elsewhere 1

  • #122898 fix: profile terminal.cwd beats the desktops inherited workspace cwd · done elsewhere → a9972dc
Closed issues · 3
  • Closed#40334Desktop: profile switch ignores terminal.cwd, uses wrong profile's working directory
  • Closed#52589Desktop app-global workspace overrides profile-specific terminal.cwd
  • Closed#54990[Bug]: Profile switch in Desktop app leaves shell CWD stuck in previous profile's workspace
How this cluster was grouped
  • Link evidence (partly structural links): 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.20: 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
apps/desktop
Merged PRs
#40892, #122310
Closed issues
3
Attempts
1 PR closed without merging
Fix link checked
25 Sep 2026

For your agent

The same facts, typed.