Persistent prompt injection happens when an agent writes instructions it read in a hostile web page, document or message into its own long-term memory or a reusable skill. Saved, they reload in later sessions, for other users and in scheduled jobs. The fix: treat every memory and skill write as a code change, gated, attributed and reversible.
This post is the defender's version of that story. It explains how a self-improving agent turns a one-off injection into a durable one, what the two best known research papers (AgentPoison and MINJA) actually demonstrated, where the write paths are in Nous Research's open source Hermes Agent, and how to set up approval, audit and rollback so that you can undo a bad lesson without throwing away the good ones. Every Hermes setting below was checked against the project's own documentation on 23 October 2026, when the latest tagged release on GitHub was v0.22.5 (published 23 October 2026). Hermes ships several releases a week, so check the docs again before you copy a setting.
Key takeaways
- A normal prompt injection ends when the conversation ends. An agent that writes its own memory or skills can carry the injected instruction forward, because the agent itself becomes the author of the persistent artifact.
- AgentPoison (arXiv 2407.12784) showed that a few poisoned records in a retrieval-based memory or knowledge base can steer an agent when a trigger appears. MINJA (arXiv 2503.03704) showed that an attacker who only sends ordinary queries can get malicious records into a shared memory bank. Neither paper tested file-based memory or skills, and both used research agents, not production assistants.
- A skill is a stronger persistence point than a memory note. It loads as a procedure the agent follows, it can carry scripts and reference files, it can declare environment variables that pass into sandboxes, and it can be shared or attached to cron jobs.
- In Hermes, memory and skill writes are not gated by default:
memory.write_approvalandskills.write_approvalboth default tofalse, and the content scanner for agent-written skills (skills.guard_agent_created) is off by default. Memory entries are always scanned for injection patterns before acceptance. - The Hermes skill ledger records every skill mutation with an actor, before and after file hashes and, where available, the session that triggered it. That ledger, plus version control on the memory files, is the basis for detection and for surgical rollback.
- OWASP names this risk ASI06 Memory and Context Poisoning in its Top 10 for Agentic Applications 2026. MITRE ATLAS lists it as AML.T0080 AI Agent Context Poisoning, with a Memory sub-technique, under the Persistence tactic.
What persistent prompt injection is, and how it differs from the usual kind
Prompt injection is any input that makes a model act on instructions its operator did not intend. OWASP's LLM01:2025 entry splits it into direct injection, where the user types the instructions, and indirect injection, where the model reads them from an external source such as a website or file. NIST's adversarial machine learning taxonomy (NIST AI 100-2 E2025, March 2025) describes indirect prompt injection in section 3.4 as an attack enabled by what it calls resource control: the attacker manipulates something the system reads, rather than talking to the application directly.
In both cases the classic damage is bounded by the conversation. The model reads the hostile text, it may do something it should not, and then the session ends and the context is gone. MITRE ATLAS notes in its LLM Prompt Injection entry (AML.T0051) that the effects "can persist throughout an interactive session", which is exactly the limit: throughout the session, not after it.
Persistent prompt injection removes that limit. The injected content, or a paraphrase of it, ends up in a store that the agent reloads later:
| Property | Session-bound injection | Persistent injection |
|---|---|---|
| Where the instruction lives | The live context window | A memory file, a skill, a knowledge base, a stored transcript or a config file |
| Who writes it into storage | Nobody; it is only read | Usually the agent itself, through its own memory or skill tools |
| When it takes effect | The current turn or session | The next session, and every session after until someone removes it |
| Who is affected | The person in that conversation | Anyone whose session loads the same store, including scheduled jobs |
| How you clean up | End the session | Find the entry, find what else it changed, roll back, rotate anything it could reach |
| Closest framework names | OWASP LLM01, ATLAS AML.T0051 | OWASP ASI06 and T1, ATLAS AML.T0080 and AML.T0081 |
We have covered memory poisoning in general in AI agent memory poisoning and memory hardening and the wider risk set in AI agent memory security risks. For the distinction between the two injection routes, see direct vs indirect prompt injection. This post is narrower: it is about the learning loop, the machinery that lets an agent decide on its own what to remember and which procedures to save.
How indirect prompt injection becomes persistent once an agent can write memory or skills
A self-improving agent has a write path into its own future context. That is the feature. Hermes Agent's README calls its design a "built-in learning loop": the agent curates its own memory, creates skills after complex tasks and improves them during use. The persistence problem is that the same write path serves whoever managed to influence the agent's judgment during the session.
The chain has four links.
- Exposure. The agent reads untrusted content as part of normal work: a web page it was asked to summarise, a README in a repository it cloned, an issue comment, a document attached to a chat, a tool result from an MCP server.
- Adoption. The model treats some of that content as guidance about how to behave, or as a fact worth keeping. This is the ordinary indirect injection step.
- Write. The agent, or a background process acting on the agent's behalf, decides the guidance is a lesson worth saving and writes it with its memory or skill tool. No attacker touches the file system. The agent's own authority does the write.
- Reload. At the next session start, or when a matching task comes up, the saved artifact is loaded into the system prompt or the active context, where it carries more weight than the original web page ever did, because it now looks like the agent's own notes or its owner's procedure.
OWASP's Top 10 for Agentic Applications 2026 describes the third and fourth links almost word for word. Its ASI06 entry lists context-window manipulation, where crafted content injected into a conversation is later summarised or saved to memory and goes on contaminating decisions after the original session ends. Its mitigation list then makes a point that matters a great deal for self-improving agents: prevent automatic re-ingestion of an agent's own generated outputs into trusted memory, to avoid what it calls bootstrap poisoning. A learning loop is, by design, a mechanism for re-ingesting the agent's own outputs. That does not make learning loops wrong. It means the write step is where the control has to sit.
Three properties of real agents make the reload step worse than it sounds.
Elevation of trust. Content in a system prompt is usually treated as more authoritative than content in a tool result. Hermes injects MEMORY.md and USER.md into the system prompt at session start, according to its memory documentation. A sentence that would have been one untrusted line in a web page becomes part of the agent's standing instructions.
Reach beyond the original user. Memory in Hermes is scoped per profile, according to the same documentation, and the sessions documentation describes per-user sessions inside group chats by default. Reading the two pages together, several people talking to the same profile through a messaging gateway each get their own conversation but share that profile's memory and skills. A lesson written during one person's session therefore loads in another person's next session. That is our reading of how the two documented scopes combine, not a statement Hermes makes in one place; test it on your own deployment.
Unattended execution. Hermes cron jobs can attach one or more skills, which load exactly as they would in a chat, according to the cron documentation. A poisoned skill attached to a scheduled job runs with no human in the conversation at all.
What AgentPoison and MINJA showed, and under what assumptions
Two papers are cited most often for memory poisoning in agents. Both are useful, and both are narrower than their headlines. The table summarises them in the authors' own terms, from the arXiv records and paper text fetched on 23 October 2026.
| Paper | arXiv ID and first version | What gets poisoned | Attacker access assumed | Agents and models tested | Reported result (authors' terms) | What it did not show |
|---|---|---|---|---|---|---|
| AgentPoison: Red-teaming LLM Agents via Poisoning Memory or Knowledge Bases (Chen, Xiang, Xiao, Song, Li) | 2407.12784, v1 17 July 2024 | Records in a retrieval-based long-term memory or RAG knowledge base | Partial write access to the memory or knowledge base, and white-box access to the retriever's embedder for trigger optimisation (the authors report transfer to other embedders, including a black-box one) | An autonomous driving agent, a knowledge-intensive QA agent and a healthcare agent (EHRAgent), with GPT-3.5 and LLaMA3-70b backbones and two retriever types | Average attack success rate above 80 percent, benign performance impact under 1 percent, poison rate under 0.1 percent | Injection by an outside user through conversation; file-based memory; skills; production assistants |
| Memory Injection Attacks on LLM Agents via Query-Only Interaction, MINJA (Dong, Xu, He, Li, Tang, Liu, Liu, Xiang) | 2503.03704, v1 5 March 2025, v5 12 February 2026 | Records in a memory bank of past interactions used as demonstrations | None beyond being a regular user who sends queries and sees outputs; the agent must use a shared memory bank, or an isolated one where identity can be disguised | RAP on Webshop (GPT-4 and GPT-4o based), EHRAgent on MIMIC-III and eICU, a QA agent on MMLU | Average injection success rate of 98.2 percent and average attack success rate of 76.8 percent across settings | Skills or procedural memory; agents whose memory is not retrieved by similarity; agents with write approval |
AgentPoison, conceptually
AgentPoison treats poisoning as a backdoor. The attacker adds a small number of malicious demonstrations to the memory or knowledge base and optimises a trigger so that any user instruction containing it pulls those demonstrations out of retrieval with high probability. Without the trigger, the agent behaves normally, which is why benign accuracy barely moves. The authors stress that no model training or fine-tuning is needed, and they report that the optimised trigger evaded two candidate defences they tried: perplexity checks and query rephrasing.
The key assumption is write access to the store. AgentPoison does not explain how the poisoned records get there; it assumes they can, by analogy with editing a public page that a knowledge base later indexes. For a self-improving agent, the learning loop is one realistic answer to that question, which is why the paper matters here even though it never tests a learning loop.
MINJA, conceptually
MINJA removes the write-access assumption. The attacker never touches the memory bank. Instead, the attacker sends queries crafted so that the agent's own reasoning produces records that link a future victim's query to the attacker's desired reasoning steps, then gradually removes the scaffolding from the queries so the stored record looks natural and is retrieved later when a different user asks the targeted question. The paper's threat model states its main system assumption plainly: a shared memory bank supports all users' queries, which the authors describe as common in agent frameworks.
The defence section is the part defenders should read. The authors tested prompt-level detection with GPT-4o as a judge. A detection prompt written for one agent caught nearly all malicious records on that agent but none on two others; a general prompt caught some on every agent but flagged 34 of 50 benign records on one of them. They also report that malicious and benign records were entangled in embedding space, so similarity-based filtering of memory did not separate them. In plain terms, content scanning helps, but it trades missed attacks for false alarms, and it is not a boundary.
What neither paper shows, and how to read them for Hermes-style agents
Both papers studied memories that are retrieved by embedding similarity and used as worked examples. Hermes's built-in memory works differently: the two small files are injected whole at session start, with no retrieval step, and its session_search tool is a full-text search over stored sessions. That changes the attacker's problem rather than removing it. A trigger is not needed to get a line from MEMORY.md into context, because every line is always there; on the other hand, the store is tiny (2,200 characters for agent notes and 1,375 for the user profile, per the memory docs) and every write is scanned. Neither paper measured attacks on procedural skills at all. Treat both as evidence that memory writes driven by untrusted interactions are a real attack surface, not as measurements of any specific product.
Why a skill is a stronger persistence point than a memory note
A memory note is a sentence. A skill is a small package of instructions and code. In Hermes, a skill is a directory under ~/.hermes/skills/ with a SKILL.md (sections for when to use it, the procedure, pitfalls and verification) and optional references/, templates/, scripts/, examples/ and assets/ folders, according to the skills documentation. Several properties follow from that shape.
| Property | Memory entry (MEMORY.md, USER.md) | Agent-written skill |
|---|---|---|
| How it loads | Always, into the system prompt at session start | When relevant, as instructions the agent follows step by step |
| Size | Capped at 2,200 and 1,375 characters | SKILL.md can run to tens of thousands of characters; the linter warns past about 24,000; reference files load on demand |
| Can carry code | No | Yes, helper scripts in scripts/ that the procedure calls |
| Can request credentials | No | Yes, required_environment_variables in frontmatter; when set, those variables pass through to terminal, execute_code and container backends on load |
| Default write gate | None (memory.write_approval: false) | None (skills.write_approval: false) |
| Default content scan on agent writes | Always on for memory entries | Off (skills.guard_agent_created: false) |
| Can be shared | Per profile | Publishable to a repository, installable from hubs and URLs, vendored into project repositories, attachable to cron jobs |
| Precedence games | None | A project skill with the same name overrides a profile or bundled skill inside that repository |
Three of those rows deserve a closer look.
Instructions, not facts. The skills docs describe a skill as the instructions for doing a class of task, in order, with the commands that work. A poisoned memory note has to persuade the model on every turn. A poisoned step inside a procedure is followed because the agent wrote the procedure for exactly this situation.
Code and credentials. A skill can ship a helper script and declare environment variables. Hermes's security docs explain that both terminal and execute_code strip sensitive variables from child processes by default, and that a skill's declared variables are registered for passthrough when the skill is loaded and the values exist. That is a reasonable design for legitimate skills that need an API key. It also means a change to a skill's frontmatter is a change to which secrets reach the sandbox, and should be reviewed like one.
The background review can edit any skill. The curator documentation is explicit that the background self-improvement review "can patch, rewrite, and add support files to any skill it learns from", including hand-written, hub-installed, bundled, external and pinned skills. Pinning only protects a skill from deletion and archiving. Every such write is recorded in the skill ledger, and skills.write_approval can stage them for review. If you have not turned that gate on, the review's edits land directly.
For a wider look at injected instructions inside shared skill catalogues, see our post on prompt injection prevalence in agent skill ecosystems, and for the related problem in tool metadata, tool poisoning attacks.
Which paths into the learning loop exist in Hermes
The table below lists every path we found in the Hermes documentation by which content can enter memory, skills or other context that reloads later. Defaults are as documented on 23 October 2026.
| Path | What triggers it | What it can write | Default gate | Scanning | Recorded where |
|---|---|---|---|---|---|
| Foreground memory write | The agent calls the memory tool (add, patch, remove) during a turn | MEMORY.md or USER.md | None; with memory.write_approval: true, inline prompt in the CLI, staged elsewhere | Always: injection, credential exfiltration and SSH backdoor patterns, invisible Unicode | The files themselves; staged writes under /memory pending |
| Foreground skill write | The agent calls skill_manage (create, patch, delete, write_file, remove_file), often after a multi-step task, a recovered error or a user correction | Any skill it can write, plus supporting files | None; with skills.write_approval: true, staged under ~/.hermes/pending/skills/ | Advisory linter always; dangerous-pattern scanner only with skills.guard_agent_created: true | Skill ledger, actor agent |
| Background self-improvement review | A forked copy of the agent reviews the conversation after a turn | Memory entries; new skills; patches and support files on existing skills | Same two gates; staged patch and remove on memory are recorded with their full target | As above | Skill ledger, actor curator; memory notifications in chat (on by default) |
| Curator | An inactivity check, by default every 7 days (curator.interval_hours: 168) | Prune pass archives idle agent-created skills; LLM consolidation (off by default) patches and merges them | Snapshot before consolidation; skills.write_approval applies to its skill_manage calls | As above | Skill ledger; snapshots under ~/.hermes/skills/.curator_backups/ |
/learn | You point the agent at a URL, a directory, a document or the current conversation | A new or extended skill, often with references/ files | skills.write_approval applies | As for foreground skill writes | Skill ledger; skill recorded as created_by: learn |
| Skills Hub and URL installs | hermes skills install from a hub, a GitHub repository, a well-known endpoint or a direct URL | A third-party skill bundle | Install confirmation; dangerous verdicts stay blocked for community sources | Built-in security scanner on the whole quarantined bundle | skills/.hub/lock.json with source URL, content hash, scanner version and findings |
| Project-local skills | A repository you open contains skills | Skills with the highest precedence inside that repository | Not loaded until hermes skills trust | Every project skill is scanned; dangerous ones are quarantined; re-scanned when content changes | Tagged [project] in the skill index |
| Context files | AGENTS.md, .cursorrules, SOUL.md and similar in the working directory | System prompt content for that session | None | Heuristic injection scan; a hit blocks the file, except your own SOUL.md, which loads with a warning | Warning in /context |
| Profile distributions | hermes profile install from a git URL | A third party's SOUL.md and profile files | None | The docs say the SOUL.md is copied without a scan or approval prompt; a scanner hit at load time still blocks it when the distribution owns the file | distribution.yaml |
| Session transcripts | Every CLI and messaging session | Rows in ~/.hermes/state.db, searchable later with session_search | None | None documented for stored transcripts | state.db |
| External memory providers | memory.provider set to a plugin backend | Whatever that provider stores | Provider specific | Provider specific | Provider specific |
A few notes on that table.
The background review's cadence is described two ways in the docs. The memory page says it runs after a turn; the curator page describes a periodic review pass roughly every 10 agent turns. We quote both; check your version's behaviour in the gateway notifications before you rely on either.
The README and our own earlier research notes describe session search as using LLM summarisation; the current memory documentation says search queries return actual messages with "no LLM summarization". We used the memory documentation for this post. Either way, stored transcripts matter for persistence: a hostile page the agent summarised last month is still in state.db and can come back through session_search.
The docs also say something important about who gets the curator's attention. Only skills marked as created by the background review are curated automatically. Skills the foreground agent wrote at your request, including through /learn, are recorded as created_by: learn and left alone by the curator, which is a sensible ownership rule but also means a skill learned from a hostile page will not age out on its own.
What defences exist, and what their limits are
No single control solves this. OWASP's LLM01 entry says outright that it is unclear whether fool-proof prevention for prompt injection exists. The practical goal is to make durable writes rare, visible, attributable and reversible. Here is how the main classes of control hold up.
| Control | What it catches | What it misses | Hermes setting or feature |
|---|---|---|---|
| Content scanning of memory writes | Known injection phrasing, credential exfiltration commands, SSH backdoor patterns, invisible Unicode | Paraphrased or benign-looking guidance; anything that is harmful only in combination with a later task | Always on for memory entries |
| Content scanning of agent-written skills | Dangerous keyword patterns in skill content | Same as above; the docs say it was turned off by default because legitimate workflows tripped it | skills.guard_agent_created: true |
| Write approval | Every write, if a person reads it | Approval fatigue; staged diffs truncated in chat bubbles; approvers who do not know what a bad procedure looks like | memory.write_approval, skills.write_approval |
| Provenance tags | Lets you ask who wrote this, when, from which session | Does not judge content; ledger is telemetry, and a failed ledger write does not block the mutation | Skill ledger actor and session evidence; created_by; hub lock.json; [project] tag; [auto] tag on staged memory writes |
| Separation of user and agent memory | Keeps the user profile and the agent's notes in distinct, separately sized stores | Both still load into the same system prompt for the same profile | MEMORY.md and USER.md; memory_enabled and user_profile_enabled |
| Separate profiles | Stops one person's or one job's lessons loading for another | Costs you shared learning | One profile per user group or purpose |
| Read-only skill sets | A process that cannot write a directory cannot poison it | Hermes says external skill directories are not a write-protection boundary if they are writable; pinning blocks deletion, not edits | Filesystem permissions on external_dirs; skills.disabled |
| Trust gates on imported content | Untrusted project skills and hub skills | Content that changes after trust; the scan is a heuristic | hermes skills trust, scan-time quarantine, dangerous verdict blocks |
| Egress and site policy | Limits which pages the agent can read in the first place | Allowed sites can still host hostile text | security.website_blocklist |
| Headless deny | Stops scheduled jobs from approving dangerous commands on their own | Does not stop a poisoned skill from giving bad advice or writing files | approvals.cron_mode: deny, unattended_mode: deny (both default) |
Content scanning
Hermes scans memory entries before accepting them and scans context files before loading them. The security documentation is candid about the method: these patterns are "heuristics, not semantic intent detection". The MINJA results above show the same trade-off from the research side. Scanning is worth having because it removes the lazy attacks and the obvious encoding tricks, but an instruction phrased as a helpful workflow tip will usually pass.
Write approval
Write approval is the strongest control Hermes offers here, because it moves the decision from the model to a person. With skills.write_approval: true, every skill write is staged regardless of whether it came from a foreground turn or the background review, survives restarts and can be reviewed as a full unified diff with /skills diff. With memory.write_approval: true, interactive CLI writes prompt inline and everything else is staged for /memory pending. The limits are human: approvals that arrive every few turns get rubber-stamped. Two habits help. Review staged writes in a batch at a natural boundary, such as the end of a task, and read diffs on the CLI or in the pending JSON file rather than in a chat bubble, where the docs note diff output is truncated.
Provenance tags
Provenance prevents nothing, but it makes every other control work after the fact. Hermes records each skill mutation in an append-only JSON Lines ledger at ~/.hermes/skills/.curator_ledger.jsonl with the actor (curator, agent or user), the action, evidence including the triggering session where available, and before and after manifests of file paths and SHA-256 hashes. Hub installs record their source URL and content hash in lock.json. OWASP's ASI06 mitigations ask for exactly this: source attribution and detection of suspicious updates. What Hermes does not tag, as far as the docs describe, is which external content a session read before a write. You have to reconstruct that from the session transcript.
Separation of user and agent memory, and read-only skill sets
Hermes keeps two memory stores with different purposes and caps, and its docs warn against pointing two agent processes at the same Hermes home. That is good hygiene, not a trust boundary: both files load into the same system prompt for the same profile. The real separation control is the profile, so give a shared gateway or a feed-reading cron job its own. For a vetted team skill set, the cleanest defence is to make it unwritable by the agent process and change it only through reviewed commits; Hermes's docs warn that external skill directories are not a write-protection boundary when the process can write them, so rely on operating system permissions. Steps 4 and 5 below show both.
Step by step: set up the learning loop so that writes are gated, logged and reversible
This walkthrough hardens a single Hermes profile. It assumes a Linux or macOS host and a current Hermes release; check every key against your version's configuration reference first.
Step 1: back up the current state
Several later steps change agent state. Take a backup first and keep it outside the Hermes home.
# Stop running gateways or sessions for this profile first, then:
BACKUP=~/hermes-backup-$(date +%Y%m%d-%H%M%S).tar.gz
tar -czf "$BACKUP" -C ~ .hermes/memories .hermes/skills .hermes/config.yaml
tar -tzf "$BACKUP" | head # confirm the archive is readable
hermes curator backup --reason "before-hardening"
The archive deliberately leaves out ~/.hermes/.env, which holds API keys. Store the archive with the same care as any file that reveals how your agent behaves.
Step 2: turn on the write gates and the skill scanner
Edit ~/.hermes/config.yaml. The keys below are documented in the Hermes configuration, memory, skills, curator and security pages.
memory:
write_approval: true # stage or prompt every memory write, including the background review
skills:
write_approval: true # stage every skill_manage write under ~/.hermes/pending/skills/
guard_agent_created: true # scan agent-written skill content for dangerous patterns
ledger: true # keep the skill mutation ledger (default true)
ledger_max_bytes: 0 # 0 = never auto-compact or drop old ledger entries
project_discovery: true # keep project skills visible but untrusted until you trust them
curator:
enabled: true
consolidate: false # keep the LLM consolidation pass off (default)
backup:
enabled: true # snapshot ~/.hermes/skills before mutating runs
display:
memory_notifications: verbose # show what changed, not just that something changed
approvals:
mode: manual # prompt for every dangerous command instead of an LLM judgement
cron_mode: deny # default; headless jobs never auto-approve dangerous commands
unattended_mode: deny # default; webhook and API sessions likewise
security:
website_blocklist:
enabled: true
domains:
- "*.internal.example.com"
You can also flip the gates at runtime with /memory approval on and /skills approval on, which persist the setting. Setting ledger_max_bytes: 0 keeps every ledger entry; the default 5 MB cap drops the oldest entries once divergent history exceeds it, which is fine for most people but not for an investigation that starts weeks later.
Step 3: put memory and skills under version control
The ledger covers skills. Memory files have no equivalent history, so add one. A local git repository gives you diffs, blame and revert for both.
cd ~/.hermes
git init -q
cat > .gitignore <<'EOF'
*
!.gitignore
!memories/
!memories/**
!skills/
!skills/**
skills/.curator_backups/
skills/.archive/
**/__pycache__/
EOF
git add .gitignore memories skills
git -c user.name="hermes-snapshot" -c user.email="hermes@localhost" commit -qm "baseline after review"
git log --oneline | head -3
The ignore file is an allowlist: it excludes everything (including .env, config.yaml with any inline secrets, and state.db) and then re-includes only memory and skills. Commit on a schedule, for example from a cron entry on the host that runs git add -A memories skills && git commit -qm "snapshot $(date -Iseconds)" every hour, so each change has a timestamp you can line up with session history.
Step 4: split profiles by trust
Give anything that reads untrusted content without you watching its own profile: a messaging gateway that several people can reach, a cron job that summarises external feeds, a research profile that browses freely. The memory documentation describes memory as scoped per profile by design, so a lesson learned in the research profile cannot load in your coding profile. Keep write_approval on in the shared profiles even if you relax it in a personal one.
Step 5: make the vetted skill set read-only
If your team maintains a reviewed skill set, keep it in a repository, deploy it as an external directory and remove write permission for the user that runs Hermes.
# Deploy a reviewed, tagged release of the team skill set
git clone --depth 1 --branch v1.4.0 https://git.example.com/platform/agent-skills.git /opt/agent-skills
sudo chown -R root:root /opt/agent-skills
sudo chmod -R a-w /opt/agent-skills
ls -ld /opt/agent-skills # confirm no write bit for the hermes user
skills:
external_dirs:
- /opt/agent-skills
Remember the precedence order from the skills docs: project skills, then local skills in ~/.hermes/skills/, then skills.create_dir, then external directories. A local or project skill with the same name shadows your read-only copy, and Hermes logs the shadowed one as a warning. Check those warnings.
Step 6: treat the team skill repository like code
Skills with scripts are software. Give the repository the same controls as any other: required review, signed commits or signed release artifacts, pinned dependencies for any helper scripts, and an SBOM.
# Require signed commits for maintainers (SSH signing shown)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Pin helper-script dependencies by hash and install only what is pinned
pip-compile --generate-hashes -o scripts/requirements.txt scripts/requirements.in
pip install --require-hashes -r scripts/requirements.txt
# Produce a CycloneDX SBOM for the skill set's Python dependencies
cyclonedx-py requirements scripts/requirements.txt -o agent-skills.cdx.json
# Sign the release tarball keylessly and verify it on the agent host
tar -czf agent-skills-v1.4.0.tar.gz -C /path/to/repo .
cosign sign-blob --bundle agent-skills-v1.4.0.sigstore.json agent-skills-v1.4.0.tar.gz
cosign verify-blob --bundle agent-skills-v1.4.0.sigstore.json \
--certificate-identity "release@example.com" \
--certificate-oidc-issuer "https://accounts.example.com" \
agent-skills-v1.4.0.tar.gz
A GitHub Actions job can make the review rule concrete by failing a pull request when a skill gains a new script or a new required_environment_variables entry without a label from a security reviewer:
name: skill-review-gate
on: pull_request
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Flag new scripts and credential requests
run: |
BASE=${{ github.event.pull_request.base.sha }}
NEW_SCRIPTS=$(git diff --name-only --diff-filter=A "$BASE"...HEAD -- '*/scripts/*' | wc -l)
NEW_ENV=$(git diff "$BASE"...HEAD -- '*/SKILL.md' | grep -c '^+.*required_environment_variables' || true)
echo "new scripts: $NEW_SCRIPTS, new env declarations: $NEW_ENV"
if [ "$NEW_SCRIPTS" -gt 0 ] || [ "$NEW_ENV" -gt 0 ]; then
if ! echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | grep -q 'security-reviewed'; then
echo "Needs the security-reviewed label"; exit 1
fi
fi
Step 7: review staged writes on a rhythm
With the gates on, writes queue up. Review them at the end of each task rather than mid-flow.
/memory pending
/memory approve <id>
/memory reject <id>
/skills pending
/skills diff <id>
/skills approve <id>
/skills reject <id>
Read every staged skill diff on the CLI. Reject anything that adds a step you did not ask for, sends data somewhere new, changes which credentials a skill declares, or adds a script whose purpose the diff does not make obvious.
How to detect a poisoned skill or memory entry after the fact
Detection is a matter of answering three questions: what changed, who changed it, and what the agent was reading when it did.
What changed
For skills, start with the ledger and Hermes's own drift checks.
hermes curator ledger --limit 50 # newest skill mutations, with actor and action
hermes curator ledger --skill deploy-staging --limit 50
hermes curator status # managed versus unmanaged skills
hermes skills check # hub skills whose upstream content changed
hermes skills audit # re-scan all hub-installed skills
hermes skills list-modified # bundled skills edited locally
For memory, use the git history from step 3.
cd ~/.hermes
git log --since="14 days ago" --stat -- memories skills
git log -p -- memories/MEMORY.md | less
git diff HEAD~24 -- skills/ | grep -E '^\+\+\+|^\+.*(required_environment_variables|scripts/)'
Who changed it
The ledger's actor field separates the background review and curator from foreground agent calls and from your own CLI actions. The small script below summarises the ledger by actor and flags mutations that touched scripts or a SKILL.md, so you can see unusual bursts. The field names follow the curator documentation; adjust them if your ledger lines differ.
#!/usr/bin/env python3
"""Summarise the Hermes skill ledger by actor and flag script or SKILL.md changes."""
import json
import sys
from collections import Counter
from pathlib import Path
ledger = Path.home() / ".hermes/skills/.curator_ledger.jsonl"
by_actor = Counter()
flagged = []
for line_no, raw in enumerate(ledger.read_text(encoding="utf-8").splitlines(), 1):
try:
entry = json.loads(raw)
except json.JSONDecodeError:
continue
actor = entry.get("actor", "unknown")
by_actor[actor] += 1
paths = set()
for side in ("before", "after"):
for item in entry.get(side) or []:
if isinstance(item, dict) and "path" in item:
paths.add(item["path"])
touched = [p for p in paths if "/scripts/" in p or p.endswith("SKILL.md")]
if touched and actor in ("agent", "curator"):
flagged.append((line_no, entry.get("id"), actor, entry.get("action"),
(entry.get("evidence") or {}).get("session_id"), sorted(touched)))
print("mutations by actor:", dict(by_actor))
for row in flagged[-25:]:
print(row)
sys.exit(0)
What the agent was reading
Where the ledger records a triggering session, open that session's history. Export with redaction if you need to share it with someone else.
hermes sessions list --limit 50
hermes sessions export suspect.jsonl --session-id 20261020_141502_ab12cd34 --redact
Look for the external content the agent consumed shortly before the write: pages fetched with web_extract, files read from a cloned repository, MCP tool results, messages from another participant in a group chat. The memory and skill entry itself will rarely look hostile; the session usually explains it.
Signals worth alerting on, without needing to know what an attack looks like:
- A skill write by
agentorcuratorwithin a few turns of the agent reading a page or repository you have never used before. - A skill that gained a script, a new outbound host, or a new
required_environment_variablesentry. - A memory entry phrased as a standing instruction about behaviour rather than a fact about you or your environment.
- A local or project skill that shadows a vetted one of the same name.
- Bursts of background-review edits to skills unrelated to the current task.
- A
SOUL.mdthat/contextlists as loaded with a matched injection pattern you did not write.
How to clean up and roll back without losing legitimate learning
The goal is to remove the poisoned lesson and anything it changed, while keeping the hundreds of useful lessons the agent learned the same week. That rules out the blunt option of wiping memory and skills. Work from narrowest to widest.
Step 1: freeze and back up
Stop the gateway and any cron jobs for the affected profile so nothing writes while you work, then take a fresh backup. Rollback commands below change agent state.
hermes cron list
hermes cron pause <job_id_or_name> # repeat for each job in the affected profile
tar -czf ~/hermes-incident-$(date +%Y%m%d-%H%M%S).tar.gz -C ~ .hermes/memories .hermes/skills
hermes curator backup --reason "incident-before-cleanup"
Step 2: roll back single skill mutations
Warning: the next command restores files from the ledger's blob store and removes files the mutation created. Confirm the entry ID against the ledger output and keep the backup from step 1.
hermes curator ledger --skill deploy-staging --limit 20
hermes curator rollback <entry-id> # restore that one mutation's before-state
Single-entry rollback restores only the files that one mutation touched, takes a safety ledger entry of the current state first and fails closed if it cannot. Because foreground deletes are ledgered too, it can also bring back a legitimate skill that a poisoned procedure told the agent to delete. If many mutations from one session are suspect, roll each back newest first.
Step 3: whole-tree rollback only when the damage is broad
Warning: this replaces the live skills tree with a snapshot. Hermes takes a pre-rollback snapshot first, so a mistaken rollback can be undone with --id, but your step 1 backup is still your safety net.
hermes curator rollback --list
hermes curator rollback --id <snapshot-id>
The docs note that snapshots exclude .archive/, the ledger and .hub/, and that the default prune-only pass takes no snapshot at all. If the poisoned edits came from the background review rather than a consolidation pass, single-entry rollback from the ledger is usually the better tool.
Step 4: remove the memory entry
For memory, revert the specific commit in your state repository, or edit the file by hand with the agent stopped. Warning: git revert rewrites the working copy of the memory files; confirm the commit first.
cd ~/.hermes
git log --oneline -- memories/MEMORY.md | head
git show <commit> -- memories/MEMORY.md # confirm this is the poisoned change
git revert --no-edit <commit>
If a staged write is still pending, reject it with /memory reject <id>. Because memory is a frozen snapshot loaded at session start, start a new session after the fix (/new on gateways) so the corrected file is what loads.
Step 5: chase the side effects
A poisoned skill may have done things before you found it. Check, in this order:
- Which credentials could it reach? Any variable declared in its frontmatter and set in the profile was passed into sandboxes when it loaded. Rotate those keys.
- Which cron jobs reference it? The curator docs note that skills referenced by any cron job, including paused ones, are exempt from archiving, so a poisoned skill attached to a schedule will not age out. Edit the job with
hermes cron edit <job_id> --remove-skill <name>after you have confirmed the skill name. - What did its scripts install or write? Use the project's checkpoint history (
/rollbacklists checkpoints when checkpoints are enabled) and your normal dependency review for any repository the agent touched. - Did the lesson spread? Search other skills for the same new step or host, and search memory in other profiles that share a gateway.
Step 6: re-scan, re-baseline, resume
hermes skills audit
hermes curator status
cd ~/.hermes && git add -A memories skills && git commit -qm "post-incident baseline"
Resume cron jobs one at a time and leave write_approval on for at least a few weeks.
What OWASP and MITRE ATLAS call these techniques, and how they map to controls
Framework names help when you write a risk register or explain the problem to an auditor. The mapping below uses OWASP's Top 10 for LLM Applications 2025, the OWASP Agentic AI Threats and Mitigations guide (February 2025), the OWASP Top 10 for Agentic Applications 2026 (December 2025), the MITRE ATLAS technique data and NIST AI 100-2 E2025.
| What happens | OWASP | MITRE ATLAS | NIST AI 100-2 E2025 | Primary controls |
|---|---|---|---|---|
| Hostile text in a page, file or tool result steers the agent | LLM01:2025 Prompt Injection (indirect) | AML.T0051.001 LLM Prompt Injection: Indirect | 3.4 Indirect prompt injection | Site policy, separate untrusted content, least privilege, human approval for high-risk actions |
| The agent writes that guidance into its memory | ASI06 Memory and Context Poisoning; Agentic Threats T1 Memory Poisoning | AML.T0080 AI Agent Context Poisoning, sub-technique AML.T0080.000 Memory (Persistence tactic) | 3.5 Security of agents | Memory write approval, scanning, version control, profile separation |
| Instructions persist in a long-running chat thread | ASI06 | AML.T0080.001 Thread | 3.5 | Session boundaries (/new), shorter-lived sessions on gateways |
| A shared memory or skill set spreads the change to other users or agents | ASI06; Agentic Threats T12 Shared Memory Poisoning | AML.T0080.000 Memory | 3.5 | One profile per trust zone, read-only vetted skill sets |
| The agent rewrites a skill, context file or other configuration it loads later | ASI06; ASI04 Agentic Supply Chain Vulnerabilities for third-party skills | AML.T0081 Modify AI Agent Configuration (Persistence and Defense Evasion) | 3.5 | Skill write approval, ledger review, signed and reviewed skill repositories |
| A skill or tool the agent uses is altered to keep influence | ASI04 | AML.T0110 AI Agent Tool Poisoning (Persistence) | 3.5 | Hub provenance and hash checks, hermes skills check and audit, pinned versions |
| Poisoned records sit in a retrieval store the agent searches | LLM04:2025 Data and Model Poisoning; LLM08:2025 Vector and Embedding Weaknesses | AML.T0070 RAG Poisoning; AML.T0099 AI Agent Tool Data Poisoning | 3.4 | Source allowlists, provenance on indexed content, periodic re-indexing from trusted sources |
Both frameworks already treat the write path as an attack surface. OWASP's ASI06 entry covers persistent corruption that "propagates across sessions" and keeps one-time injection under LLM01, and ATLAS files AI Agent Context Poisoning under its Persistence tactic.
One caution on sources: the ATLAS data file we read is the dist/ATLAS.yaml in the mitre-atlas/atlas-data repository, which now carries a notice that it is deprecated in favour of a newer data format. The technique IDs above matched that file on 23 October 2026; confirm them on atlas.mitre.org before you cite them in a formal document.
Common mistakes
- Leaving both write gates off on a shared profile. The defaults suit a single user experimenting at a terminal. They do not suit a gateway that several people can message or a cron job that reads the internet.
- Assuming pinning protects a skill. Pinning blocks deletion and archiving. Patches and edits still go through, including from the background review.
- Assuming the scanner is a boundary. Hermes calls its checks heuristics, and the MINJA authors' detection experiments show the trade-off between missed attacks and false alarms. Use scanning to catch the obvious and approval to catch the rest.
- Approving diffs in a chat bubble. The docs warn that diff output is truncated on messaging platforms. Read full diffs on the CLI or in the pending JSON.
- Wiping memory and skills after an incident. It removes the evidence and the legitimate learning together. Roll back the specific entries from the ledger and git history instead.
- Forgetting the credentials. A skill that declared environment variables had them passed into its sandboxes whenever it loaded. Rotate them, even if you found no sign of use.
- Running long-lived gateway sessions forever. Memory loads at session boundaries; a session that never ends keeps any injected instruction in context, which is the ATLAS Thread case.
- Trusting a repository once and forgetting. Hermes re-scans project skills when content changes, but trust itself is per repository. Untrust repositories you no longer work in with
hermes skills untrust.
Where Safeguard fits, honestly
Safeguard does not scan, govern or monitor Hermes Agent, and nothing in Safeguard reads an agent's memory files. The connection is narrower and practical. A self-improving agent writes scripts, adds dependencies and authors skills that end up in repositories and container images, and those artifacts are ordinary software supply chain components. Safeguard generates and analyses SBOMs in SPDX and CycloneDX for repositories, images and SBOM files, signs and attests builds with SLSA, Sigstore and in-toto, and evaluates policies and gates, all generally available, so a team skill repository or an agent-modified service can go through the same checks as any other code. Safeguard's AI Gateway (an LLM firewall that inspects prompts for direct and indirect injection), MCP Guardrail and AI-BOM discovery are in early access, and none of them is a Hermes integration.
FAQ
Is Hermes Agent safe to run?
Hermes ships with an eight-layer security model that includes dangerous command approval, container isolation and prompt injection scanning of memory entries and context files, according to its security documentation. For persistence specifically, the safest setup turns on memory.write_approval and skills.write_approval, enables skills.guard_agent_created, separates profiles by trust and keeps the skill ledger. As with any agent that runs commands, the local backend has no isolation, so use a container backend for untrusted work.
What is persistent prompt injection?
It is a prompt injection whose effect survives the session. The injected instruction ends up in a store that reloads later, such as long-term memory, a skill, a configuration file or a long-running chat thread, so it keeps influencing the agent in future sessions and sometimes for other users.
Can prompt injection survive a new chat session?
Yes, if the agent saved something from the poisoned session. Starting a new session clears the context window, but in Hermes, memory files are injected into the system prompt at every session start and skills load when relevant, so anything written to them comes back. Starting a new session is still the right move after you remove the entry, because memory is a frozen snapshot taken at session start.
What is the difference between memory poisoning and skill poisoning?
Memory poisoning corrupts facts or preferences the agent keeps in context. Skill poisoning corrupts a procedure the agent follows, which can include helper scripts and declared credentials. In Hermes, memory entries are small and always scanned; agent-written skills are larger, are not scanned by default and can be attached to scheduled jobs, which makes them the stronger persistence point.
Does Hermes Agent scan memory for prompt injection?
Yes. The memory documentation says entries are scanned for prompt injection, credential exfiltration and SSH backdoor patterns, and for invisible Unicode, before they are accepted, and matching content is blocked. Agent-written skills are scanned only if you set skills.guard_agent_created: true.
How do I see what my agent has learned?
Run hermes curator ledger for skill changes with their actor and action, /memory pending and /skills pending for staged writes, and hermes curator status for which skills the curator manages. The /journey command shows a learning timeline of created and used skills. For memory history, keep the memory files in a local git repository.
How do I undo something the agent learned?
For a skill, find the mutation with hermes curator ledger and run hermes curator rollback with that entry's ID after taking a backup. For memory, revert the change in your git history or edit the file with the agent stopped, then start a new session. Rotate any credentials a suspect skill declared and remove it from cron jobs.
What did the MINJA paper show?
MINJA (arXiv 2503.03704) showed that an attacker who only sends ordinary queries can get malicious records into an agent's shared memory bank, which are later retrieved for a different user's query. Across three research agents and four victim-target settings, the authors report an average injection success rate of 98.2 percent and attack success rate of 76.8 percent. It did not test skills or agents with write approval.
What is AgentPoison?
AgentPoison (arXiv 2407.12784) is a red-teaming method that plants a few malicious demonstrations in an agent's retrieval-based memory or knowledge base and optimises a trigger that retrieves them. The authors report an average attack success rate above 80 percent with less than 1 percent impact on benign performance and a poison rate under 0.1 percent. It assumes the attacker can write to the store.
What do OWASP and MITRE call memory poisoning in AI agents?
OWASP calls it ASI06 Memory and Context Poisoning in the Top 10 for Agentic Applications 2026, and T1 Memory Poisoning in its Agentic AI Threats and Mitigations guide. MITRE ATLAS lists AML.T0080 AI Agent Context Poisoning with Memory and Thread sub-techniques under the Persistence tactic, and AML.T0081 Modify AI Agent Configuration for changes to files an agent loads.
Fix it without waiting for upstream
A self-improving agent installs packages and writes scripts that become part of your codebase, and the skills it authors are small software projects with their own dependencies. Its own dependency tree, plugins and skill scripts are open source components that receive CVEs like any other, so they need the same patching discipline as the rest of your code.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos involve Hermes Agent or inspect an agent's memory. They show how Safeguard handles the code and dependencies that agents produce, and how an assistant can use Safeguard as one of its tools.
The first demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Safeguard is installed from Claude's connector directory rather than a hand-written MCP configuration, and the tool selection step is the same least-privilege idea this post recommends for agents: grant only the tools the task needs. Once a team skill repository's SBOM is in Safeguard, the same connection lets an assistant ask which projects contain a given component and version.
Click through it at your own pace, or open it full screen.
The second demo is a real Safeguard CLI session: asked to fix a lodash CVE, the assistant reads the finding and advisory, asks before each command, edits package.json, runs npm install and npm test with your approval, and pushes nothing. That approve-each-step pattern is the same discipline this post asks for on memory and skill writes, applied here to a dependency fix in a repository an agent may have changed.
Click through it at your own pace, or open it full screen.
The third demo sets up the local runner on your own machine, approves it with a device code, then scans from the terminal. For a team that keeps its vetted agent skills in a repository, that means scanning the skill scripts' dependencies on your own hardware before a release is tagged and deployed read-only.
Click through it at your own pace, or open it full screen.
To try the CLI, download Safeguard and follow the CLI documentation.
Quick check
Why is an agent-written skill a stronger persistence point than a memory entry?
A skill loads as a procedure the agent follows, can carry helper scripts, can declare environment variables that pass into sandboxes when it loads, and can be shared or attached to cron jobs. In Hermes, agent-written skills are also not content-scanned by default, while memory entries always are.
After you find a poisoned skill change, what should you do before rolling it back?
Pause cron jobs and gateways for the profile, take a backup of memories and skills, and find the mutation in hermes curator ledger. Then roll back that single entry, rotate any credentials the skill declared, and remove it from any cron jobs that reference it.
Sources
- Hermes Agent repository and README, Nous Research
- Hermes Agent releases (GitHub API), latest v0.22.5 published 23 October 2026
- Hermes docs: Persistent Memory
- Hermes docs: Skills System
- Hermes docs: Curator
- Hermes docs: Security
- Hermes docs: Configuration
- Hermes docs: Cron
- Hermes docs: Sessions
- Hermes docs: Checkpoints and rollback
- AgentPoison: Red-teaming LLM Agents via Poisoning Memory or Knowledge Bases, arXiv 2407.12784
- Memory Injection Attacks on LLM Agents via Query-Only Interaction (MINJA), arXiv 2503.03704
- OWASP LLM01:2025 Prompt Injection
- OWASP LLM04:2025 Data and Model Poisoning
- OWASP Agentic AI: Threats and Mitigations (February 2025)
- OWASP Top 10 for Agentic Applications for 2026 (December 2025)
- MITRE ATLAS technique data, mitre-atlas/atlas-data
- MITRE ATLAS
- NIST AI 100-2 E2025, Adversarial Machine Learning: A Taxonomy and Terminology of Attacks and Mitigations
Last reviewed: 24 October 2026. Hermes Agent ships several releases a week; check the linked documentation for current defaults and commands before changing your configuration.