feat(context-files): add @<path> include expansion (CAAMP-style transitive includes) - #20876
Open
kryptobaseddev wants to merge 11 commits into
Open
feat(context-files): add @<path> include expansion (CAAMP-style transitive includes)#20876kryptobaseddev wants to merge 11 commits into
kryptobaseddev wants to merge 11 commits into
Conversation
Remove VOLUME keyword for Railway deployment
Fix permissions for hermes user on /opt/data volume
Fix volume permissions in entrypoint for Railway deployment
Introduce agent/context_includes.py — a pure, provider-neutral helper that resolves CAAMP-style @<path> directives in context files. The module is intentionally decoupled from prompt_builder so the same expander can be reused by any caller (cron jobs, gateway hooks, batch_runner, etc.) without pulling the full prompt-builder graph. Highlights: - Recursive expansion with hard depth cap (CONTEXT_INCLUDE_MAX_DEPTH=5) - Cycle detection via visited-set; A->B->A terminates - Path resolution: absolute / ~ / $VAR / relative-to-including-file - @<path> inside fenced code blocks (```, ~~~) stays inert so docs that describe the syntax don't trigger expansion - Inline @mentions in prose are NOT expanded; only line-prefixed tokens - Pluggable scanner + truncator hooks so callers (e.g. prompt_builder) can inject their own injection-guard and size-cap logic - Emits structured HTML-comment markers for missing/cycle/max-depth/ unreadable cases so the agent can see exactly what was substituted Adds 22 unit tests in tests/agent/test_context_includes.py covering path resolution, pattern matching, recursion, cycles, depth caps, code fences, scanner/truncator hooks, and edge cases (directory targets, relative paths inside included files, custom max_depth).
Wire the new agent.context_includes.expand_includes helper into all five context-file loaders so AGENTS.md, .hermes.md, CLAUDE.md, .cursorrules (+ .cursor/rules/*.mdc), and SOUL.md now transparently expand @<path> directives before injection-scanning and truncation. This brings Hermes to parity with Claude Code, Cursor, and other harnesses that already honor @-includes, and fixes the long-standing gap where a project AGENTS.md containing '@~/.agents/AGENTS.md' would be injected as the literal string instead of the global file's contents. Loading pipeline per file is now: read -> expand_includes -> scan_context_content -> truncate_content The scanner runs against the *expanded* text so prompt-injection patterns hidden in an included file are still blocked. The truncator runs both per-include (inside expand_includes) and over the final assembled context, preserving the existing 20K char-per-source budget. Adds 11 integration tests in TestExpandIncludes covering simple include, nested A->B->C expansion, cycle detection, missing files, depth limit, injection blocking through includes, code-fence inertness, relative-path resolution, ~ expansion, inline-mention non-expansion, and direct helper invocation.
…T pattern Add an '@-Includes (Single Source of Truth)' section to the context files guide covering: - Syntax rules (line-prefixed only; inline @mentions ignored) - Path resolution table (~, $VAR, relative-to-including-file) - Recursion + safety rails (depth cap, cycle detection, missing files, unreadable files, per-include size cap, injection scanning) - Marker format (<!-- @include-begin/end/missing/cycle/max-depth -->) - Code-fence inertness so docs that show the syntax are safe - The 'global ~/.agents/AGENTS.md as SSoT' pattern with a worked example Also adds tip NousResearch#7 to the AGENTS.md best-practices block recommending @-includes for cross-project shared rules.
Subdirectory hints discovered mid-session via tool calls (read_file, terminal, search_files, etc.) now run through the same agent.context_includes.expand_includes pipeline as the startup context-file loader. Without this, a nested AGENTS.md containing '@~/.agents/AGENTS.md' would be injected into the tool result with the literal @-line, while the same file at the project root WOULD be expanded — an inconsistency that broke the single-source-of-truth pattern as soon as the agent descended into a subtree with its own context file. The expander is invoked with the project's prompt-injection scanner already wired in; the size cap stays at the existing _MAX_HINT_CHARS (8K) since hint text is appended to a tool result, not the system prompt. Adds 2 tests in TestSubdirectoryHintsIncludeExpansion covering: - A nested AGENTS.md with @./shared.md expands inline - Missing includes leave a marker without breaking the hint
teknium1
reviewed
Jul 12, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for the well-tested include-expansion implementation. The underlying gap is still present on current main: agent/prompt_builder.py:1882-1885 reads and scans literal AGENTS.md content without expansion.
Problems
agent/context_includes.py:112-117accepts~, environment-expanded, and absolute paths, then reads the result into the model context. This conflicts with the containment rationale inagent/subdirectory_hints.py:172-195, which prevents loading context outside the workspace.- Current main’s truncation contract now carries
context_lengthandread_path(agent/prompt_builder.py:1779-1816); the PR’s older wrapper does not preserve those per-source semantics for included files. - The diff also changes the compatibility shim
docker/entrypoint.shfrom executable to mode100644; current docs still support it for downstream entrypoint overrides (website/docs/user-guide/docker.md:499-500).
Suggested changes
- Establish an explicit trusted-root or user-approved mechanism for external includes, preserve subdirectory containment, and test external-path rejection.
- Integrate with the current context-budget/truncation API and remove unrelated Docker changes.
Automated hermes-sweeper review.
| Relative paths resolve against the *including* file's directory so | ||
| nested includes behave intuitively when files move. | ||
| """ | ||
| expanded = os.path.expandvars(os.path.expanduser(raw)) |
Contributor
There was a problem hiding this comment.
This lets a repository-controlled context file resolve ~, environment variables, and arbitrary absolute paths before the expander reads the target into the model prompt. That bypasses the existing workspace-containment policy in agent/subdirectory_hints.py:172-195; please restrict this to an explicit trusted root or user-approved external source and add a rejection test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class
@<path>include expansion to all context files (AGENTS.md,.hermes.md,CLAUDE.md,.cursorrules,.cursor/rules/*.mdc,SOUL.md). This brings Hermes to parity with Claude Code, Cursor, and other harnesses that already honor@-includes, and unlocks the "single source of truth" pattern where one canonical instruction file lives in~/.agents/AGENTS.md(or anywhere) and is referenced from every project.Before this change, a project
AGENTS.mdcontaining:…was injected into the system prompt with the literal string
@~/.agents/AGENTS.mdinstead of the global file's contents. Users had to copy/symlink shared rules into every repo, or accept that their CAAMP-style includes only worked in Claude Code, not Hermes.Now the expander recursively resolves the include, follows transitive includes (the global file can reference its own SSoT files), and produces a fully-assembled context block — with depth caps, cycle detection, and prompt-injection scanning applied to every layer.
Design
Implementation is split into a pure, reusable module (
agent/context_includes.py) and a thin wiring layer inagent/prompt_builder.py. The expander module has zero dependencies on prompt-builder internals — the prompt-injection scanner and size-truncator are passed in as hooks. This means cron jobs, gateway hooks, batch_runner, or third-party plugins can reuse the same expander without dragging in the full prompt-builder graph.Public API (agent/context_includes.py)
Returns the expanded content with each
@<path>directive replaced by the wrapped contents of the target file.Loading pipeline (per context file)
The scanner runs against the expanded text so prompt-injection patterns hidden in an included file are still blocked. The truncator runs both per-include (inside
expand_includes) and over the final assembled context, preserving the existing 20K char-per-source budget.Syntax
A line that contains only
@<path>(with optional leading/trailing whitespace) is replaced inline with the contents of the referenced file. Inline@mentionsin prose (e.g. "ask @bob") are not expanded.@/abs/path.md@~/path.md$HOME@$VAR/path.mdor@${VAR}/path.md@relative/path.mdSafety rails
max_depthare stopped<!-- @max-depth: <path> --><!-- @cycle: already-included <path> --><!-- @missing: <path> ... --><!-- @unreadable: <path> (<err>) -->[BLOCKED: <path> contained ...]@<path>inside```or~~~blocks stays as literal textSuccessfully-expanded chunks are wrapped with begin/end markers so users (and the agent) can see exactly which file each piece came from:
Tests
33 new tests, all passing, zero regressions in the existing suite.
tests/agent/test_context_includes.py— 22 unit tests for the standalone module:resolve_include_path: absolute, relative,~,$VARINCLUDE_PATTERN: line-only matching, leading whitespace, inline rejectionexpand_includes: simple, nested A→B→C, cycles, missing files, max-depth marker, custom max-depth, code fences (```and~~~), scanner/truncator hook invocation, relative-path resolution inside included files, directory-target handling, marker presencetests/agent/test_prompt_builder.py::TestExpandIncludes— 11 integration tests verifying the wiring through the fullbuild_context_files_promptpipeline.Full
tests/agent/suite: 2385 passing (5 pre-existing failures from missing optional depsbotocore/fastapionmain, unrelated to this PR).End-to-end verification
Tested against a real project that uses the SSoT pattern (project
AGENTS.md→@~/.agents/AGENTS.md→@~/.cleo/templates/CLEO-INJECTION.md):Backward compatibility
100% backward compatible. Files without
@-include directives behave exactly as before (the regex pass is a no-op). Existing tests untouched.Files changed
Commits
feat(context-includes): add reusable @-include expander module— pure module + 22 testsfeat(prompt-builder): expand @<path> includes in all context files— wiring + 11 testsdocs(context-files): document @-include syntax, safety rails, and SSoT pattern— user-facing docsOut of scope (potential follow-ups)
context_files.expand_includes: false)@~/.agents/skills/*.md) — current pattern is one file per directive@!<path>opt-out from injection scanning — probably never wanted