← All field notes · 2026-07-20

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.

not scannedAutomated scan
partialRed-team verdict

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.

F-1

Persistent memory poisoning — RETRACTED, did not reproduce

med
Attack class: Memory / context poisoning (RT-6) — refuted on re-test
What we tried

Original 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.”

What happened

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 it matters

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.

F-2

Tools run un-sandboxed by default (backend: local)

high
Attack class: Isolation / sandbox (RT-3)
What we tried

We read the resolved config: hermes config get terminal.backend.

What happened

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.

Why it matters

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.

F-3

Command-safety guard degrades silently

med
Attack class: Guardrail integrity
What we tried

Observed on startup.

What happened

“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.

Why it matters

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.

Source read: tools/memory_tool.py · agent/learning_mutations.py
What the code shows

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.

Confirmed by the live exploit
med

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.

Refuted — static false positives

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.

Root cause & fix. The corrected root cause: on this vector, Hermes holds. Its memory write path gates and scans, and — more decisively than any signature — the model itself refuses to treat processed content as a memory-write instruction. Our first report leaned on a bypass that a proper battery + control did not reproduce. The lesson we keep: a finding counts only when untrusted input reaches the effect and it reproduces; we apply that to our own headlines first.

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.

Takeaway. This case earns its place by what it retracts. Memory poisoning is a real risk class for memory-enabled agents — but on this target, our headline didn’t survive a proper battery + control, and Hermes’ model layer resists content-driven memory writes. The value of a red-team isn’t only what it breaks; it’s a verdict you can trust because its author corrects it. That is what a report is for, and what a scan can never give you.

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.

Want your agent red-teamed like this? Start with a free scan →