# NousResearch/hermes-agent · Agent context-file discovery loading Hermes install-tree AGENTS.md

## Open now

- 1 issue, 1 PR open on GitHub.
- #47753: \[Bug\]: Hermes CLI launcher loads hermes-agent/AGENTS.md as project context for external workspace tasks
- PR #47798: fix(agent): show full file path in context file truncation warning

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.

## TL;DR

- 7 member(s): 1 open issue(s), 1 closed issue(s), 1 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #65485. Verify it addresses your case.
- **Recommended:** read PR #65485 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-65485`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

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

## Closed / duplicate issues

- #64590: \[Bug\]: Context-file discovery falls back to the Hermes install tree and loads its contributor AGENTS.md as project context

## Prior & competing PRs

- **#64603: fix(agent): guard context-file discovery against Hermes install tree**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#64590`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#65485: fix(agent): never load the install-tree AGENTS.md as project context (salvage #64611)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - 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`
  - fix hunks:
  - `agent/prompt_builder.py`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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."""
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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)
         "41409874+2751738943@users.noreply.github.com": "2751738943",  # PR #54785 salvage (tui: post-turn completion ownership routing)
         "Burgunthy@users.noreply.github.com": "Burgunthy",  # PR #20096 salvage (gateway: profile-based routing for inbound messages)
         "75556242+webtecnica@users.noreply.github.com": "webtecnica",  # PR #63360 salvage (nous: restore inference-api base_url)
    ```
  - `tests/agent/test_prompt_builder.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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 ""
     
    ```
- **#47798: fix(agent): show full file path in context file truncation warning**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `f80381c456c5c6289651110f638b5d1aa22d51c0`
  - files: `agent/prompt_builder.py`, `tests/agent/test_prompt_builder.py`
- **#64611: fix(agent): never load the install-tree AGENTS.md as project context**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#65485`
- **#65542: fix(agent): never load the install-tree AGENTS.md from the unconfigured-cwd fallback**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `33513991b`, `244f70aae`, `#65485`

## Fix status

- Merged to main in PR #65485 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-16`.
- Subsystem: `agent`
