Summary
The context-file threat scanner is negation-blind: a SOUL.md containing the (perfectly benign, in fact anti-role-play) instruction
Don't pretend to be a specialist you're not.
matches the role_pretend pattern in tools/threat_patterns.py:
(rf'pretend\s+{_FILLER}(you\s+are|to\s+be)\s+', "role_pretend", "context"),
and because agent/prompt_builder.py replaces the entire context file with a placeholder on any single finding —
findings = _scan_for_threats(content, scope="context")
if findings:
logger.warning("Context file %s blocked: %s", filename, ", ".join(findings))
return f"[BLOCKED: {filename} contained potential prompt injection ({', '.join(findings)}). Content not loaded.]"
— the agent silently loses its whole persona/instruction file. The only signal is a single WARNING log line at prompt-build time. In our fleet, one agent ran persona-less for ~3 weeks before we noticed the warning while debugging something unrelated.
Reproduction
from tools.threat_patterns import scan_for_threats
content = "Route HR asks to the HR agent. Don't pretend to be a specialist you're not.\n"
print(scan_for_threats(content, scope="context")) # ['role_pretend']
Observed on hermes-agent 0.18.0 (pattern present since at least 0.17.x).
Other context-scope patterns have the same negation/imperative blindness, e.g. a persona file saying "You are now the ops commander for X" trips role_hijack, and "never answer without restrictions" style phrasing trips remove_filters. Ordinary operator-authored instruction prose is a large false-positive surface.
Suggestions (any subset would help)
- Negation awareness or tighter anchoring for the role-play patterns — e.g. skip matches preceded by
don't|do not|never|avoid within a few tokens, or anchor on second-person imperative forms actually used in injections.
- Fail smaller: redact/annotate the offending line(s) instead of dropping the whole file. Losing one sentence is recoverable; silently losing the entire persona changes agent behavior wholesale.
- Surface the block: a startup banner,
hermes doctor check, or health-status flag when a context file is blocked. A log-level warning is easy to miss on a long-running gateway, and the failure mode (agent behaves generically) doesn't obviously point at the scanner.
- Optionally, a way for operators to mark their own config-managed context files as trusted (the threat model for scraped web content is very different from an operator-authored SOUL.md that ships with the deployment).
Happy to provide more detail from our deployment if useful.
Summary
The context-file threat scanner is negation-blind: a SOUL.md containing the (perfectly benign, in fact anti-role-play) instruction
matches the
role_pretendpattern intools/threat_patterns.py:and because
agent/prompt_builder.pyreplaces the entire context file with a placeholder on any single finding —— the agent silently loses its whole persona/instruction file. The only signal is a single
WARNINGlog line at prompt-build time. In our fleet, one agent ran persona-less for ~3 weeks before we noticed the warning while debugging something unrelated.Reproduction
Observed on hermes-agent 0.18.0 (pattern present since at least 0.17.x).
Other context-scope patterns have the same negation/imperative blindness, e.g. a persona file saying "You are now the ops commander for X" trips
role_hijack, and "never answer without restrictions" style phrasing tripsremove_filters. Ordinary operator-authored instruction prose is a large false-positive surface.Suggestions (any subset would help)
don't|do not|never|avoidwithin a few tokens, or anchor on second-person imperative forms actually used in injections.hermes doctorcheck, or health-status flag when a context file is blocked. A log-level warning is easy to miss on a long-running gateway, and the failure mode (agent behaves generically) doesn't obviously point at the scanner.Happy to provide more detail from our deployment if useful.