{"repo":"NousResearch/hermes-agent","public_id":"47753","display_label":"Agent context-file discovery loading Hermes install-tree AGENTS.md","summary":"Most members concern context-file discovery in the agent picking up the Hermes install tree's contributor AGENTS.md as project context, via bug reports and several fix PRs (including a salvage). One member is more tangential, about showing the full file path in a context file truncation warning.","subsystem":"agent","counts":{"members":7,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":65485,"merged_at":"2026-07-16T11:32:24+00:00"},"resolution":{"state":"partially_resolved","by":"pr-65485"},"freshness":{"state":"verified","last_verified_at":"2026-07-16T11:32:24+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.25523809523809526},"fix_edges":[{"issue_number":64590,"pr_number":65485,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-16T11:32:24+00:00"}],"members":[{"kind":"issue","number":47753,"title":"[Bug]: Hermes CLI launcher loads hermes-agent/AGENTS.md as project context for external workspace tasks","url":null,"state":"open"},{"kind":"issue","number":64590,"title":"[Bug]: Context-file discovery falls back to the Hermes install tree and loads its contributor AGENTS.md as project context","url":null,"state":"closed"},{"kind":"pr","number":47798,"title":"fix(agent): show full file path in context file truncation warning","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"f80381c456c5c6289651110f638b5d1aa22d51c0","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":64603,"title":"fix(agent): guard context-file discovery against Hermes install tree","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64590","number":64590}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":64611,"title":"fix(agent): never load the install-tree AGENTS.md as project context","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#65485","number":65485}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":65485,"title":"fix(agent): never load the install-tree AGENTS.md as project context (salvage #64611)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/prompt_builder.py","agent/runtime_cwd.py","agent/system_prompt.py","scripts/release.py","tests/agent/test_prompt_builder.py","tests/agent/test_runtime_cwd.py","tests/agent/test_system_prompt.py"],"fix_hunks":[{"file":"agent/prompt_builder.py","hunks":["@@ -1957,6 +1957,7 @@ def build_context_files_prompt(\n     cwd: Optional[str] = None,\n     skip_soul: bool = False,\n     context_length: Optional[int] = None,\n+    allow_install_tree_fallback: bool = False,\n ) -> str:\n     \"\"\"Discover and load context files for the system prompt.\n ","@@ -1978,17 +1979,43 @@ def build_context_files_prompt(\n     \"\"\"\n     if cwd is None:\n         cwd = os.getcwd()\n+        cwd_is_fallback = True\n+    else:\n+        cwd_is_fallback = False\n \n     cwd_path = Path(cwd).resolve()\n     sections = []\n \n-    # Priority-based project context: first match wins\n-    project_context = (\n-        _load_hermes_md(cwd_path, context_length)\n-        or _load_agents_md(cwd_path, context_length)\n-        or _load_claude_md(cwd_path, context_length)\n-        or _load_cursorrules(cwd_path, context_length)\n-    )\n+    # Never let a FALLBACK-picked directory inside the Hermes install/source\n+    # tree gain system-prompt authority. A backend that self-spawns into that\n+    # tree (the desktop app default) would otherwise load this repo's\n+    # contributor AGENTS.md as authoritative project context (#64590). An\n+    # explicitly configured cwd is honored verbatim — the Hermes tree is a\n+    # legitimate workspace when the user deliberately points a session at it —\n+    # and CLI-style surfaces pass allow_install_tree_fallback=True because\n+    # their launch dir IS the user's shell cwd (developing Hermes in-tree).\n+    from agent.runtime_cwd import _is_install_tree\n+\n+    if (\n+        cwd_is_fallback\n+        and not allow_install_tree_fallback\n+        and _is_install_tree(cwd_path)\n+    ):\n+        logger.warning(\n+            \"skipping project-context discovery: working-directory resolution \"\n+            \"fell back to the Hermes install tree (%s) — set terminal.cwd to \"\n+            \"your project directory\",\n+            cwd_path,\n+        )\n+        project_context = \"\"\n+    else:\n+        # Priority-based project context: first match wins\n+        project_context = (\n+            _load_hermes_md(cwd_path, context_length)\n+            or _load_agents_md(cwd_path, context_length)\n+            or _load_claude_md(cwd_path, context_length)\n+            or _load_cursorrules(cwd_path, context_length)\n+        )\n     if project_context:\n         sections.append(project_context)\n "]},{"file":"agent/runtime_cwd.py","hunks":["@@ -10,15 +10,36 @@\n contextvar; CLI/cron fall through to `TERMINAL_CWD`/launch cwd.\n \"\"\"\n \n+import logging\n import os\n from contextvars import ContextVar, Token\n from pathlib import Path\n from typing import Any\n \n+logger = logging.getLogger(__name__)\n+\n _UNSET: Any = object()\n \n _SESSION_CWD: ContextVar = ContextVar(\"HERMES_SESSION_CWD\", default=_UNSET)\n \n+# The Python package/source root (this file lives at <root>/agent/runtime_cwd.py).\n+# When a backend is launched from, or self-spawns into, this tree (the desktop\n+# app default), an os.getcwd() fallback would inject this repo's contributor\n+# AGENTS.md as authoritative project context. Context discovery must never\n+# resolve here.\n+_PACKAGE_ROOT = Path(__file__).resolve().parent.parent\n+\n+\n+def _is_install_tree(p: Path) -> bool:\n+    # True only when p IS the package root or sits inside it. Ancestors of the\n+    # package root (a user home that happens to contain the checkout, a --user\n+    # site-packages parent) are legitimate workspaces and must not be blocked.\n+    try:\n+        p = p.resolve()\n+    except Exception:\n+        return False\n+    return p == _PACKAGE_ROOT or _PACKAGE_ROOT in p.parents\n+\n \n def set_session_cwd(cwd: str | None) -> Token:\n     \"\"\"Pin the logical cwd for the current context.\"\"\"","@@ -42,21 +63,38 @@ def resolve_agent_cwd() -> Path:\n         p = Path(override).expanduser()\n         if p.is_dir():\n             return p\n+        logger.warning(\"configured working directory does not exist: %s\", override)\n     raw = os.environ.get(\"TERMINAL_CWD\", \"\").strip()\n     if raw:\n         p = Path(raw).expanduser()\n         if p.is_dir():\n             return p\n+        logger.warning(\"TERMINAL_CWD does not exist: %s\", raw)\n     return Path(os.getcwd())\n \n \n def resolve_context_cwd() -> Path | None:\n     # None means \"no configured cwd\": build_context_files_prompt then falls back\n-    # to the launch dir (os.getcwd()) — correct for the local CLI. The gateway\n-    # avoids slurping its install dir by setting TERMINAL_CWD (see system_prompt.py)\n-    # or, per session, the _SESSION_CWD contextvar above.\n+    # to the launch dir (os.getcwd()), correct for a local CLI launched inside a\n+    # real project. A configured path is validated here (previously it was passed\n+    # through unchecked, diverging from resolve_agent_cwd). An explicitly\n+    # configured path is otherwise honored verbatim — including the Hermes\n+    # source tree itself, which is a legitimate workspace when the user is\n+    # developing Hermes (per-surface policy for fallback-picked directories\n+    # lives in build_context_files_prompt; see #64590).\n     override = _session_cwd_override()\n     if override:\n-        return Path(override).expanduser()\n+        p = Path(override).expanduser()\n+        if not p.is_dir():\n+            logger.warning(\"configured working directory does not exist: %s\", override)\n+        else:\n+            return p\n+        return None\n     raw = os.environ.get(\"TERMINAL_CWD\", \"\").strip()\n-    return Path(raw).expanduser() if raw else None\n+    if raw:\n+        p = Path(raw).expanduser()\n+        if not p.is_dir():\n+            logger.warning(\"TERMINAL_CWD does not exist: %s\", raw)\n+        else:\n+            return p\n+    return None"]},{"file":"agent/system_prompt.py","hunks":["@@ -463,9 +463,16 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)\n         # CLI), None lets build_context_files_prompt fall back to the launch\n         # dir — the user's real cwd there, but the install dir for the gateway\n         # daemon, which is why the gateway sets TERMINAL_CWD.\n+        #\n+        # allow_install_tree_fallback: for cli/tui the launch dir IS the\n+        # user's shell cwd, so an in-tree fallback is a deliberate choice\n+        # (developing Hermes). Every other surface (desktop chat panel,\n+        # gateway daemons) self-spawns into the install tree, where the\n+        # fallback would inject this repo's contributor AGENTS.md (#64590).\n         context_files_prompt = _r.build_context_files_prompt(\n             cwd=resolve_context_cwd(), skip_soul=_soul_loaded,\n-            context_length=_ctx_len)\n+            context_length=_ctx_len,\n+            allow_install_tree_fallback=agent.platform in (\"cli\", \"tui\"))\n         if context_files_prompt:\n             context_parts.append(context_files_prompt)\n "]},{"file":"scripts/release.py","hunks":["@@ -46,6 +46,7 @@\n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n     \"[redacted:email]\": \"SAMBAS123\",  # PR #64986 salvage (gateway: multiplex primary bot token scope)\n+    \"[redacted:email]\": \"evefromwayback\",  # PR #64611 salvage (agent: never load install-tree AGENTS.md as project context)\n     \"41409874+2751738943@users.noreply.github.com\": \"2751738943\",  # PR #54785 salvage (tui: post-turn completion ownership routing)\n     \"Burgunthy@users.noreply.github.com\": \"Burgunthy\",  # PR #20096 salvage (gateway: profile-based routing for inbound messages)\n     \"75556242+webtecnica@users.noreply.github.com\": \"webtecnica\",  # PR #63360 salvage (nous: restore inference-api base_url)"]},{"file":"tests/agent/test_prompt_builder.py","hunks":["@@ -708,6 +708,44 @@ def test_loads_agents_md(self, tmp_path):\n         assert \"Ruff for linting\" in result\n         assert \"Project Context\" in result\n \n+    def test_skips_agents_md_in_install_tree_on_fallback(self, monkeypatch, tmp_path):\n+        # A backend that FALLS BACK into the install tree (cwd=None → getcwd,\n+        # the desktop default) must not load that tree's contributor AGENTS.md\n+        # as project context. The guard keys off the package root, so point it\n+        # at a fake tree holding an AGENTS.md and getcwd into it.\n+        import agent.runtime_cwd as rt\n+\n+        monkeypatch.setattr(rt, \"_PACKAGE_ROOT\", tmp_path.resolve())\n+        (tmp_path / \"AGENTS.md\").write_text(\"Never give up on the right solution.\")\n+        monkeypatch.chdir(tmp_path)\n+        result = build_context_files_prompt(cwd=None, skip_soul=True)\n+        assert \"Never give up\" not in result\n+        assert result == \"\"\n+\n+    def test_loads_agents_md_in_install_tree_when_explicit(self, monkeypatch, tmp_path):\n+        # An EXPLICIT cwd pointing at the install tree is a deliberate user\n+        # choice (developing Hermes) — discovery must still run.\n+        import agent.runtime_cwd as rt\n+\n+        monkeypatch.setattr(rt, \"_PACKAGE_ROOT\", tmp_path.resolve())\n+        (tmp_path / \"AGENTS.md\").write_text(\"Never give up on the right solution.\")\n+        result = build_context_files_prompt(cwd=str(tmp_path), skip_soul=True)\n+        assert \"Never give up\" in result\n+\n+    def test_loads_agents_md_in_install_tree_fallback_for_cli(self, monkeypatch, tmp_path):\n+        # CLI/TUI surfaces launch from the user's shell cwd, so an in-tree\n+        # fallback there is deliberate — allow_install_tree_fallback=True\n+        # (system_prompt.py passes it for platform cli/tui) keeps discovery on.\n+        import agent.runtime_cwd as rt\n+\n+        monkeypatch.setattr(rt, \"_PACKAGE_ROOT\", tmp_path.resolve())\n+        (tmp_path / \"AGENTS.md\").write_text(\"Never give up on the right solution.\")\n+        monkeypatch.chdir(tmp_path)\n+        result = build_context_files_prompt(\n+            cwd=None, skip_soul=True, allow_install_tree_fallback=True\n+        )\n+        assert \"Never give up\" in result\n+\n     def test_loads_cursorrules(self, tmp_path):\n         (tmp_path / \".cursorrules\").write_text(\"Always use type hints.\")\n         result = build_context_files_prompt(cwd=str(tmp_path))"]},{"file":"tests/agent/test_runtime_cwd.py","hunks":["@@ -66,12 +66,21 @@ def test_returns_none_when_unset(self, monkeypatch):\n         monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n         assert resolve_context_cwd() is None\n \n-    def test_returns_nonexistent_dir_unguarded(self, monkeypatch, tmp_path):\n-        # Deliberate asymmetry vs resolve_agent_cwd: context discovery has no isdir\n-        # guard, so a missing dir is returned (not None) — discovery just finds nothing.\n+    def test_returns_none_for_nonexistent_dir(self, monkeypatch, tmp_path):\n+        # A configured but missing dir must not be returned. It previously was,\n+        # which diverged from resolve_agent_cwd and let an invalid cwd steer\n+        # context discovery. Now it is validated and drops to None.\n         missing = tmp_path / \"gone\"\n         monkeypatch.setenv(\"TERMINAL_CWD\", str(missing))\n-        assert resolve_context_cwd() == missing\n+        assert resolve_context_cwd() is None\n+\n+    def test_returns_install_tree_when_explicitly_configured(self, monkeypatch):\n+        # An EXPLICITLY configured install-tree cwd is honored verbatim — the\n+        # Hermes source tree is a legitimate workspace when the user is\n+        # developing Hermes. Only the fallback path (cwd=None → os.getcwd())\n+        # is policed, in build_context_files_prompt (#64590).\n+        monkeypatch.setenv(\"TERMINAL_CWD\", str(rt._PACKAGE_ROOT))\n+        assert resolve_context_cwd() == rt._PACKAGE_ROOT\n \n     def test_expands_leading_tilde(self, monkeypatch):\n         monkeypatch.setenv(\"TERMINAL_CWD\", \"~\")"]},{"file":"tests/agent/test_system_prompt.py","hunks":["@@ -31,7 +31,10 @@ def _captured_context_cwd(agent):\n     \"\"\"The cwd build_system_prompt_parts hands to build_context_files_prompt.\"\"\"\n     captured = {}\n \n-    def fake_context_files(cwd=None, skip_soul=False, context_length=None):\n+    def fake_context_files(\n+        cwd=None, skip_soul=False, context_length=None,\n+        allow_install_tree_fallback=False,\n+    ):\n         captured[\"cwd\"] = cwd\n         return \"\"\n \n"]}]},{"kind":"pr","number":65542,"title":"fix(agent): never load the install-tree AGENTS.md from the unconfigured-cwd fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"33513991b","number":0},{"kind":"commit","value":"244f70aae","number":0},{"kind":"pr","value":"#65485","number":65485}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}