NousResearch/hermes-agent

Agent context-file discovery loading Hermes install-tree AGENTS.md

Cluster 47753 · agent. Data as of

2 open items. Fix merged in #65485 on 16 Jul 2026. Check it covers your case.

The short answer

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

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.

Open issues1

  • Issue#47753[Bug]: Hermes CLI launcher loads hermes-agent/AGENTS.md as project context for external workspace tasks

Open PRs1

  • PR#47798fix(agent): show full file path in context file truncation warning

Fixes on record

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

  • #65485 fix(agent): never load the install-tree AGENTS.md as project context (salvage #64611)

    merged to main

    files: 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

    Show the diff
    • agent/prompt_builder.py
      @@ -1957,6 +1957,7 @@ def build_context_files_prompt(
           cwd: Optional[str] = None,
           skip_soul: bool = False,
           context_length: Optional[int] = None,
      +    allow_install_tree_fallback: bool = False,
       ) -> str:
           """Discover and load context files for the system prompt.
       
      @@ -1978,17 +1979,43 @@ def build_context_files_prompt(
           """
           if cwd is None:
               cwd = os.getcwd()
      +        cwd_is_fallback = True
      +    else:
      +        cwd_is_fallback = False
       
           cwd_path = Path(cwd).resolve()
           sections = []
       
      -    # Priority-based project context: first match wins
      -    project_context = (
      -        _load_hermes_md(cwd_path, context_length)
      -        or _load_agents_md(cwd_path, context_length)
      -        or _load_claude_md(cwd_path, context_length)
      -        or _load_cursorrules(cwd_path, context_length)
      -    )
      +    # Never let a FALLBACK-picked directory inside the Hermes install/source
      +    # tree gain system-prompt authority. A backend that self-spawns into that
      +    # tree (the desktop app default) would otherwise load this repo's
      +    # contributor AGENTS.md as authoritative project context (#64590). An
      +    # explicitly configured cwd is honored verbatim — the Hermes tree is a
      +    # legitimate workspace when the user deliberately points a session at it —
      +    # and CLI-style surfaces pass allow_install_tree_fallback=True because
      +    # their launch dir IS the user's shell cwd (developing Hermes in-tree).
      +    from agent.runtime_cwd import _is_install_tree
      +
      +    if (
      +        cwd_is_fallback
      +        and not allow_install_tree_fallback
      +        and _is_install_tree(cwd_path)
      +    ):
      +        logger.warning(
      +            "skipping project-context discovery: working-directory resolution "
      +            "fell back to the Hermes install tree (%s) — set terminal.cwd to "
      +            "your project directory",
      +            cwd_path,
      +        )
      +        project_context = ""
      +    else:
      +        # Priority-based project context: first match wins
      +        project_context = (
      +            _load_hermes_md(cwd_path, context_length)
      +            or _load_agents_md(cwd_path, context_length)
      +            or _load_claude_md(cwd_path, context_length)
      +            or _load_cursorrules(cwd_path, context_length)
      +        )
           if project_context:
               sections.append(project_context)
       
    • agent/runtime_cwd.py
      @@ -10,15 +10,36 @@
       contextvar; CLI/cron fall through to `TERMINAL_CWD`/launch cwd.
       """
       
      +import logging
       import os
       from contextvars import ContextVar, Token
       from pathlib import Path
       from typing import Any
       
      +logger = logging.getLogger(__name__)
      +
       _UNSET: Any = object()
       
       _SESSION_CWD: ContextVar = ContextVar("HERMES_SESSION_CWD", default=_UNSET)
       
      +# The Python package/source root (this file lives at <root>/agent/runtime_cwd.py).
      +# When a backend is launched from, or self-spawns into, this tree (the desktop
      +# app default), an os.getcwd() fallback would inject this repo's contributor
      +# AGENTS.md as authoritative project context. Context discovery must never
      +# resolve here.
      +_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
      +
      +
      +def _is_install_tree(p: Path) -> bool:
      +    # True only when p IS the package root or sits inside it. Ancestors of the
      +    # package root (a user home that happens to contain the checkout, a --user
      +    # site-packages parent) are legitimate workspaces and must not be blocked.
      +    try:
      +        p = p.resolve()
      +    except Exception:
      +        return False
      +    return p == _PACKAGE_ROOT or _PACKAGE_ROOT in p.parents
      +
       
       def set_session_cwd(cwd: str | None) -> Token:
           """Pin the logical cwd for the current context."""
      @@ -42,21 +63,38 @@ def resolve_agent_cwd() -> Path:
               p = Path(override).expanduser()
               if p.is_dir():
                   return p
      +        logger.warning("configured working directory does not exist: %s", override)
           raw = os.environ.get("TERMINAL_CWD", "").strip()
           if raw:
               p = Path(raw).expanduser()
               if p.is_dir():
                   return p
      +        logger.warning("TERMINAL_CWD does not exist: %s", raw)
           return Path(os.getcwd())
       
       
       def resolve_context_cwd() -> Path | None:
           # None means "no configured cwd": build_context_files_prompt then falls back
      -    # to the launch dir (os.getcwd()) — correct for the local CLI. The gateway
      -    # avoids slurping its install dir by setting TERMINAL_CWD (see system_prompt.py)
      -    # or, per session, the _SESSION_CWD contextvar above.
      +    # to the launch dir (os.getcwd()), correct for a local CLI launched inside a
      +    # real project. A configured path is validated here (previously it was passed
      +    # through unchecked, diverging from resolve_agent_cwd). An explicitly
      +    # configured path is otherwise honored verbatim — including the Hermes
      +    # source tree itself, which is a legitimate workspace when the user is
      +    # developing Hermes (per-surface policy for fallback-picked directories
      +    # lives in build_context_files_prompt; see #64590).
           override = _session_cwd_override()
           if override:
      -        return Path(override).expanduser()
      +        p = Path(override).expanduser()
      +        if not p.is_dir():
      +            logger.warning("configured working directory does not exist: %s", override)
      +        else:
      +            return p
      +        return None
           raw = os.environ.get("TERMINAL_CWD", "").strip()
      -    return Path(raw).expanduser() if raw else None
      +    if raw:
      +        p = Path(raw).expanduser()
      +        if not p.is_dir():
      +            logger.warning("TERMINAL_CWD does not exist: %s", raw)
      +        else:
      +            return p
      +    return None
    • agent/system_prompt.py
      @@ -463,9 +463,16 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
               # CLI), None lets build_context_files_prompt fall back to the launch
               # dir — the user's real cwd there, but the install dir for the gateway
               # daemon, which is why the gateway sets TERMINAL_CWD.
      +        #
      +        # allow_install_tree_fallback: for cli/tui the launch dir IS the
      +        # user's shell cwd, so an in-tree fallback is a deliberate choice
      +        # (developing Hermes). Every other surface (desktop chat panel,
      +        # gateway daemons) self-spawns into the install tree, where the
      +        # fallback would inject this repo's contributor AGENTS.md (#64590).
               context_files_prompt = _r.build_context_files_prompt(
                   cwd=resolve_context_cwd(), skip_soul=_soul_loaded,
      -            context_length=_ctx_len)
      +            context_length=_ctx_len,
      +            allow_install_tree_fallback=agent.platform in ("cli", "tui"))
               if context_files_prompt:
                   context_parts.append(context_files_prompt)
       
    • scripts/release.py
      @@ -46,6 +46,7 @@
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
           "[redacted:email]": "SAMBAS123",  # PR #64986 salvage (gateway: multiplex primary bot token scope)
      +    "[redacted:email]": "evefromwayback",  # PR #64611 salvage (agent: never load install-tree AGENTS.md as project context)
           "[email protected]": "2751738943",  # PR #54785 salvage (tui: post-turn completion ownership routing)
           "[email protected]": "Burgunthy",  # PR #20096 salvage (gateway: profile-based routing for inbound messages)
           "[email protected]": "webtecnica",  # PR #63360 salvage (nous: restore inference-api base_url)
    • tests/agent/test_prompt_builder.py
      @@ -708,6 +708,44 @@ def test_loads_agents_md(self, tmp_path):
               assert "Ruff for linting" in result
               assert "Project Context" in result
       
      +    def test_skips_agents_md_in_install_tree_on_fallback(self, monkeypatch, tmp_path):
      +        # A backend that FALLS BACK into the install tree (cwd=None → getcwd,
      +        # the desktop default) must not load that tree's contributor AGENTS.md
      +        # as project context. The guard keys off the package root, so point it
      +        # at a fake tree holding an AGENTS.md and getcwd into it.
      +        import agent.runtime_cwd as rt
      +
      +        monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
      +        (tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
      +        monkeypatch.chdir(tmp_path)
      +        result = build_context_files_prompt(cwd=None, skip_soul=True)
      +        assert "Never give up" not in result
      +        assert result == ""
      +
      +    def test_loads_agents_md_in_install_tree_when_explicit(self, monkeypatch, tmp_path):
      +        # An EXPLICIT cwd pointing at the install tree is a deliberate user
      +        # choice (developing Hermes) — discovery must still run.
      +        import agent.runtime_cwd as rt
      +
      +        monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
      +        (tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
      +        result = build_context_files_prompt(cwd=str(tmp_path), skip_soul=True)
      +        assert "Never give up" in result
      +
      +    def test_loads_agents_md_in_install_tree_fallback_for_cli(self, monkeypatch, tmp_path):
      +        # CLI/TUI surfaces launch from the user's shell cwd, so an in-tree
      +        # fallback there is deliberate — allow_install_tree_fallback=True
      +        # (system_prompt.py passes it for platform cli/tui) keeps discovery on.
      +        import agent.runtime_cwd as rt
      +
      +        monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
      +        (tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
      +        monkeypatch.chdir(tmp_path)
      +        result = build_context_files_prompt(
      +            cwd=None, skip_soul=True, allow_install_tree_fallback=True
      +        )
      +        assert "Never give up" in result
      +
           def test_loads_cursorrules(self, tmp_path):
               (tmp_path / ".cursorrules").write_text("Always use type hints.")
               result = build_context_files_prompt(cwd=str(tmp_path))
    • tests/agent/test_runtime_cwd.py
      @@ -66,12 +66,21 @@ def test_returns_none_when_unset(self, monkeypatch):
               monkeypatch.delenv("TERMINAL_CWD", raising=False)
               assert resolve_context_cwd() is None
       
      -    def test_returns_nonexistent_dir_unguarded(self, monkeypatch, tmp_path):
      -        # Deliberate asymmetry vs resolve_agent_cwd: context discovery has no isdir
      -        # guard, so a missing dir is returned (not None) — discovery just finds nothing.
      +    def test_returns_none_for_nonexistent_dir(self, monkeypatch, tmp_path):
      +        # A configured but missing dir must not be returned. It previously was,
      +        # which diverged from resolve_agent_cwd and let an invalid cwd steer
      +        # context discovery. Now it is validated and drops to None.
               missing = tmp_path / "gone"
               monkeypatch.setenv("TERMINAL_CWD", str(missing))
      -        assert resolve_context_cwd() == missing
      +        assert resolve_context_cwd() is None
      +
      +    def test_returns_install_tree_when_explicitly_configured(self, monkeypatch):
      +        # An EXPLICITLY configured install-tree cwd is honored verbatim — the
      +        # Hermes source tree is a legitimate workspace when the user is
      +        # developing Hermes. Only the fallback path (cwd=None → os.getcwd())
      +        # is policed, in build_context_files_prompt (#64590).
      +        monkeypatch.setenv("TERMINAL_CWD", str(rt._PACKAGE_ROOT))
      +        assert resolve_context_cwd() == rt._PACKAGE_ROOT
       
           def test_expands_leading_tilde(self, monkeypatch):
               monkeypatch.setenv("TERMINAL_CWD", "~")
    • tests/agent/test_system_prompt.py
      @@ -31,7 +31,10 @@ def _captured_context_cwd(agent):
           """The cwd build_system_prompt_parts hands to build_context_files_prompt."""
           captured = {}
       
      -    def fake_context_files(cwd=None, skip_soul=False, context_length=None):
      +    def fake_context_files(
      +        cwd=None, skip_soul=False, context_length=None,
      +        allow_install_tree_fallback=False,
      +    ):
               captured["cwd"] = cwd
               return ""
       

3 PRs closed without merging

From the close comments: done elsewhere 2 · merged via another change 1

  • #64603 fix(agent): guard context-file discovery against Hermes install tree · done elsewhere → #64590
  • #64611 fix(agent): never load the install-tree AGENTS.md as project context · merged via another change → #65485
  • #65542 fix(agent): never load the install-tree AGENTS.md from the unconfigured-cwd fallback · done elsewhere → 3351399, 244f70a, #65485
Closed issues · 1
  • Closed#64590[Bug]: Context-file discovery falls back to the Hermes install tree and loads its contributor AGENTS.md as project context
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.26: 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
agent
Merged PRs
#65485
Closed issues
1
Attempts
3 PRs closed without merging
Fix link checked
16 Jul 2026

For your agent

The same facts, typed.