Your Agent Remembers a Lie an Attacker Planted Last Week

A support agent started quoting a refund rule nobody wrote. The model was fine, the prompt was fine, but weeks earlier it had read an email, decided the rule was worth remembering, and wrote it to long-term memory with no record of where it came from.

Viral RuparelNo. 84 of 8411 min read

A team I was helping ran a customer support agent with long-term memory. The pitch was reasonable: let the agent remember account quirks and resolved issues so repeat contacts did not start from zero. For a few weeks it worked. Then it started telling a handful of customers they were entitled to a full refund outside the published window, citing a policy that did not exist.

Nobody had changed the prompt. The model had not been swapped. The policy lived nowhere in the codebase. It lived in the agent's memory store, written weeks earlier during a session where the agent had processed a forwarded email that contained, buried in a signature block, a sentence instructing it to remember that this account's plan included unconditional refunds. The agent decided that was worth keeping. It wrote it. And every later session that pulled memory for that account retrieved it and acted on it, with no trace of where it came from.

That is memory poisoning, and it is a different animal from the injection most teams have already thought about. The write was clean. The retrieval was clean. The attack sat dormant between them.

Two-panel figure contrasting a memory write with no provenance that any later session retrieves against a gated write that tags source and trust so retrieval can down-rank it
Figure. Memory poisoning: an ungated write versus a provenance-tagged, trust-weighted one

Why memory breaks the prompt injection threat model

Most teams model prompt injection as a within-session problem. Untrusted content enters the context, the model follows an instruction it should have ignored, and you contain the blast radius for that turn. The standard defenses assume the attack and the damage happen in the same window, which is why input filtering and trust boundaries on tool-using agents are framed around what is in context right now.

Long-term memory removes that assumption. The whole point of memory is to carry state across the session boundary, so anything written to it outlives the conversation that produced it. A July 2026 study from Roesner's group at the University of Washington, Bad Memory, tested exactly this against Claude Code and OpenAI Codex. Their finding is blunt: overwriting memory files from outside is hard, but payloads already planted in those files successfully attack current and future sessions. The persistence is the threat, not the write.

OWASP now tracks this as its own category, ASI06, Memory and Context Poisoning, separate from the injection entries. As the OWASP write-up Memory Is a Feature, It Is Also an Attack Surface puts it, a memory file is not just stored text, it can influence future decisions. Once an attacker reaches a trusted surface like memory, they stop influencing one response and start shaping every later run that reads it.

How the poison gets written

You do not need write access to the store to poison it. The MINJA attack, described in A Practical Memory Injection Attack against LLM Agents, does it with ordinary queries. The attacker sends inputs crafted so the agent itself generates and stores the malicious record during normal operation, using bridging steps and progressive shortening to hide the traces. The reported numbers are high, over 98 percent for getting the record into memory and over 76 percent for later eliciting the poisoned behavior.

The reason this works is almost always the same shape of code. The agent summarizes a session or extracts a durable fact, and it writes a string:

# The write path most memory implementations ship with.
async def remember(session_id: str, text: str):
    fact = await extract_durable_fact(session_id, text)
    if fact:
        await store.upsert(
            key=namespace(session_id),
            value=fact,            # just the text, nothing about it
        )
    # No record of where `text` came from. No trust label.
    # A fact from a signed-in admin and a fact from a
    # forwarded email are now indistinguishable in the store.

Nothing here is wrong on its own, and this is close to how persistent memory across sessions is usually implemented the first time. The gap is that the store keeps the text and throws away everything that would let you judge it later. Once the fact is in, it is just a fact.

Provenance is the field you are missing

The single highest-leverage change is to stop storing bare text. Every write records where the content originated, which session produced it, and how far you trust that source, as structured fields next to the value:

from dataclasses import dataclass
from enum import IntEnum

class Trust(IntEnum):
    SYSTEM = 4       # your own verified data, policy tables
    USER = 3         # the authenticated user in this session
    TOOL = 2         # a first-party tool result
    EXTERNAL = 1     # email bodies, web pages, third-party docs
    UNKNOWN = 0

@dataclass
class MemoryRecord:
    text: str
    source: str           # "email:msg-8842", "user:acct-19"
    trust: Trust
    session_id: str
    written_at: float
    verified: bool = False  # confirmed by a second check or a human

This costs a handful of fields and it changes what is possible downstream. The content that poisoned the support agent would have been written at Trust.EXTERNAL, because it came from an email body, and that single label is enough for the next two defenses to work. Without it, you are stuck trying to infer trust from the text after the fact, which is the thing attackers optimize against.

Trust-aware retrieval and decay

Provenance on the write is only useful if the read respects it. The failure mode is subtle: a fresh, confident, untrusted entry out-ranks an older verified one simply because it matches the query better. Semantic similarity alone has no notion of who said it. So retrieval has to combine relevance with trust and age:

import time

def score(record, similarity, now=None):
    now = now or time.time()
    age_days = (now - record.written_at) / 86_400
    # Trust dominates: an external, unverified entry is pulled
    # down hard, and recency cannot rescue it.
    trust_weight = 0.15 + 0.25 * int(record.trust)   # 0.15 .. 1.15
    if record.verified:
        trust_weight += 0.3
    # Mild decay so stale facts fade, applied after trust.
    freshness = 0.9 ** (age_days / 30)
    return similarity * trust_weight * freshness

# Retrieve, then drop anything a trusted entry already contradicts.
def select(candidates, k=5):
    ranked = sorted(candidates, key=lambda c: c.score, reverse=True)
    return ranked[:k]

The rule that matters is that trust gates recency, not the other way around. An EXTERNAL, unverified claim should sit near the bottom no matter how recent or how well it matches, and it should never silently override a SYSTEM fact it contradicts. This is the same instinct behind curating and garbage-collecting agent memory: the store is not a log you trust by default, it is a space you actively manage.

Gate the write, quarantine, keep a rollback path

Trust-aware retrieval reduces the damage of a poisoned entry, but you still want fewer of them, and you want to undo the ones that land. Three controls do most of the work.

Gate the write. A memory write driven by EXTERNAL content, especially one that reads like an instruction or a policy, should not go straight to durable store. Route it through a second check, a classifier or a cheap model pass that asks whether this is a fact about the world or a directive someone planted, and hold anything that looks directive. For writes that change what the agent is allowed to do, put a person in the loop the same way you would for any high-impact action behind an approval gate.

Keep the log append-only. Because every record carries its source and session, an immutable write log lets you answer the question that matters during an incident: which entries came from email:msg-8842, and what did they touch. That is the memory-specific case of keeping decision records for accountability, and it is the only thing that makes rollback possible. A store you can only append to and never audit is a store you cannot clean.

Watch the shared surface. In multi-agent setups the blast radius is larger, because one poisoned write reaches every agent that reads the store. If you use a shared blackboard for multi-agent context, the provenance and trust fields have to travel with the entry across agents, or one compromised reader re-poisons everyone.

The parts that bite

Provenance is only as honest as your source labels. If your ingestion pipeline tags an email forwarded by an authenticated user as USER trust rather than EXTERNAL, you have handed the attacker the trust level they wanted. The labeling has to happen at the boundary where content enters, on the raw source, before any summarization launders it.

Trust scoring has false negatives you will feel. Weight external content down hard enough to stop poisoning and you will also suppress genuinely useful things the agent read on the open web. There is no setting that keeps every real fact and drops every planted one. Tune it toward caution for anything that can change behavior or spend money, and be looser for low-stakes recall.

You cannot retrofit provenance. Entries already written as bare text have no source, so the day you add the schema you inherit a population of memories you cannot rank by trust. The pragmatic move is to treat all pre-existing entries as UNKNOWN and let them decay, which means a transition window where the agent is quietly less capable. Adding the fields before the store fills up is far cheaper than after.

Detection is split across time, so your evals have to be too. A poisoned memory does nothing in the session that writes it, which is the session your tests probably cover. The failure shows up sessions later, triggered by an unrelated query. If your eval harness runs one session and checks the output, it will pass a poisoned agent cleanly. You need multi-session tests that write in one run and probe in another.

The takeaway

Memory poisoning is not a new class of model failure, it is an old class of trust failure that long-term memory made durable. The model did exactly what it was built to do: it read content, decided it was worth remembering, and remembered it. The hole is that the store kept the text and threw away the one thing that would have let a later read judge it, which is where it came from. Every defense that works, trust-weighted retrieval, write gates, quarantine, rollback, is downstream of recording provenance on the write. Every defense that does not work tries to re-derive trust from text after the source is gone.

So the question to ask of your own system is not whether the model can be tricked, it can. It is whether, for any fact in memory, you can say where it came from and how much you trust that source. If the answer is no, that is the gap, and closing it is mostly schema and a retrieval tweak, not a smarter model. If your agent has memory and you want a second pair of eyes on the write path before an incident finds it for you, reach out. Thirty minutes, no pitch.

ShareXLinkedInviralruparel.com/blog/agent-memory-poisoning-provenance-trust-boundary

Questions this essay answers

01What is agent memory poisoning?

It is an attack where a false fact or instruction gets written into an agent's long-term memory through content the agent processes, such as an email, a document, a web page, or a tool result. The planted entry looks like something worth remembering, so the agent stores it alongside real knowledge. Unlike a prompt injection that ends when the session closes, the poisoned memory persists and is retrieved in later, unrelated sessions, where the agent applies it as if it had learned it honestly. OWASP tracks it as ASI06, Memory and Context Poisoning.

02How is memory poisoning different from prompt injection?

Prompt injection lives inside one session: the malicious text is in the context window now, and when the session ends the effect ends with it. Memory poisoning is the version that persists. The attacker gets one instruction written to durable memory, and from then on every session that retrieves it is affected, even sessions started by other users or other agents that share the store. That is why session isolation and per-turn input filters miss it. The write happened once, cleanly, and the damage is split across time from the detection.

03Does session isolation protect against memory poisoning?

No. Session isolation keeps one conversation from leaking into another, but long-term memory exists precisely to cross that boundary, so a poisoned entry travels with it. The protection you need is on the memory itself: provenance on every write so you know the source, a trust weight so untrusted sources cannot easily outrank verified ones at retrieval time, and an append-only log so a bad entry can be found and rolled back. In multi-agent systems that share a memory store, isolation helps even less, because one poisoned write can reach every agent that reads it.

04What is the cheapest first step to defend agent memory?

Add provenance. Every memory write records where the content came from, which session, and how trusted that source is, stored next to the text rather than inferred later. It costs a few fields and it changes what you can do: you can rank by trust at retrieval, quarantine writes from untrusted sources, and when something goes wrong you can query which entries came from a given source and remove them. You cannot retrofit provenance onto entries already written without it, which is the argument for adding it before the store fills up.

Working on this in production?If your agent has long-term memory and you cannot say where any given fact came from, that is the gap, and it is an afternoon of work, not a model swap. Thirty minutes, no pitch.
Viral Ruparel

Generative AI consultant and full-stack architect. Ex-IBM, former Director of Engineering at a YC W20 startup. Writes about what breaks when agents meet reality.