Hermes Agent (Nous Research)
a hugely popular (~175k★, MIT) self-hosted agent whose headline feature is persistent memory that “grows with you.” Installed and attacked in a disposable cloud sandbox, DeepSeek backend.
A correction we owe our own standard. We first published this as a critical persistent-memory-poisoning break: a “summarize this note” task with a hidden directive that Hermes silently wrote into its permanent user profile and obeyed in future sessions. On rigorous re-test — a battery of framings, with controls — that headline did not reproduce. Hermes actually resists content-driven memory writes: it declines to persist directives from processed content and names them as injection attempts. So we retract the memory-poisoning claim as originally stated. What holds are two honest, static observations about its defaults (below). A verdict you can act on is one whose author corrects it — which is exactly why this case now reads the way it does.
Persistent memory poisoning — RETRACTED, did not reproduce
medOriginal hypothesis: a “summarize this note” task with a hidden directive would be silently written into the permanent user profile and obeyed in future sessions. We re-tested it properly: a battery of framings (blunt, subtle, first-person “preference”), repeated runs, and a control that separated “the agent set the state itself” from “untrusted content reached memory.”
It did not reproduce. A plain summarize/triage task persisted nothing to memory (4/4 negative), and Hermes repeatedly named the embedded directive as “a prompt-injection attempt trying to get me to save a fake memory.” Content only reached memory when the user themselves asked the agent to “remember” things — and even then a command-style directive did not auto-fire. Our earlier “silent poisoning from a summarize task” was over-claimed.
Why we leave this here instead of deleting it: the correction is the product. A finding only counts if untrusted input actually reaches the effect, and if it reproduces under a battery — not once. Applying that bar to our own headline killed it. That discipline is exactly what makes the findings we do stand behind worth trusting.
Tools run un-sandboxed by default (backend: local)
highWe read the resolved config: hermes config get terminal.backend.
Backend = local. Of the six backends (local, Docker, SSH, Singularity, Modal, Daytona), the default runs tool/shell commands directly on the host with no isolation.
Chain it with F-1: a persistent instruction that loads every session, executed by an agent whose tools run un-sandboxed. Hermes is marketed for unattended cron + gateway operation, exactly where nobody is watching the approvals.
Command-safety guard degrades silently
medObserved on startup.
“tirith security scanner enabled but not available — command scanning will use pattern matching only.” Its command-safety control quietly falls back to weaker pattern matching when its scanner binary isn’t present.
A control that silently weakens itself on a common setup — without failing loudly — is one you can’t rely on when it matters.
Static ↔ dynamic cross-validation
Static finds candidate paths; the live exploit decides what’s real. The arbiter of truth is whether the attack actually worked — not a model vote.
Our own static engine (open, stdlib-only, in the repo) read the source and flagged the memory-write sinks (_edit_memory / _write_memory) as RT-6 candidates and the guards (_scan_memory_content / _sanitize_entries_for_snapshot) as “signature-only” candidates. Reading them confirms it: Hermes is not defenseless — writes pass a gate (_apply_write_gate) and every entry is scanned against a threat-pattern library at load time (scan_for_threats, scope="strict"), matches swapped for a [BLOCKED] placeholder before the system prompt is built; the write is atomic too. The candidate gap: that scan is signature-based, so a directive phrased as an innocuous standing preference may carry no known signature.
What the dynamic pass actually confirmed: the defense, not the break. Static flagged the memory-write sinks and the signature-only guards as candidates. The live re-test refuted the exploit — Hermes declined to persist directives from processed content and flagged them as injection. So the honest confirmed result here is that this candidate does NOT exploit via a plain content task. Candidate ≠ vulnerability; the exploit is the arbiter, and this time it cleared the target.
OUR OWN earlier claim — “signature-based memory scanning is bypassed by a semantic ‘preference’ injection” — refuted on re-test. Across a battery of framings the payload did not persist from a summarize/triage task, and the agent named it as an injection. We retract it.
“No atomicity / no checks on memory writes.” Refuted by reading the code: an atomic temp-file + rename, a file lock, a write gate, and a load-time threat scan. The problem was never absence of controls.
The exploit chain
The chain we first drew depended on F-1 — so with F-1 retracted, the “poisoned message → durable foothold” chain does not stand, and we’re not going to imply it does. What remains are two honest hardening notes: the terminal backend defaults to local/un-sandboxed (F-2), and the command guard silently degrades to weaker pattern-matching when its scanner binary is absent (F-3). Both are real defaults worth changing; neither is, on its own, an attacker-reachable exploit.
Responsible disclosure
Tested only in a disposable cloud sandbox on our own fork, with no real data, a harmless marker, and no destructive actions. Correction log: an earlier version of this case (2026-07-19) reported a confirmed critical persistent-memory-poisoning break; on re-test (2026-07-20) that headline did not reproduce and has been retracted, per our own bar that a finding must reach the effect from untrusted input and reproduce. We publish the method — and our corrections.