Agent Memory Integrity GitHub

The edits and attacks

Fourteen ways to change what an agent remembers. Eight need access to the store and are the at-rest edits, T1 to T8. Six only need to talk to the agent and are the front-door attacks. Each one below has a story, a picture of what changes, what the agent reads back, what stops it, and which stores stop it today.

genuine recordbytes changedremovedgenuine bytes movedattacker-authored
The eight edits on two users' histories user Auser B A0A1A2A3A4A5 B0B1B2B3B4B5B6 T3 remove A1T1 change A2T2 cut the tail: A4, A5T4 swap B1 and B2T5 forge B6T6 copy A2 onto B4T7 copy B0 onto B3T8 change owner of B5text untouched
Eight edits on two users' histories. T1, T5 and T8 change or add bytes. T2 and T3 remove them. T4, T6 and T7 move only genuine bytes, which is why encryption and per-record signatures do not catch them.

At rest: the attacker can write to the store

T1 Content tamper

The user told the agent last week that the wire goes to account 4471. The attacker opens the store and changes one number inside that record. Nothing else moves.

Beforeuser AA0A1A2A3A4The edituser AA0A1A2A3A4one field in A2 rewrittenWhat the agent reads backThe agent reads A2 back with thenew number and treats it as whatthe user said.

What stops it

Any authentication over each record's bytes: a MAC, a signature or an authenticated cipher. This is the edit every defence catches first.

What does not

Plain encryption without authentication, and any store that only checks that the record still parses.

Today, measured 2026-09-25

rejected on read
OpenFang model, tip-persistence fix
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T1

T2 Tail truncation

The agent made two decisions yesterday. The attacker deletes both records, so the newest record is now the one from the day before.

Beforeuser AA0A1A2A3A4The edituser AA0A1A2A3A4A3 and A4 deleted, head is now A2What the agent reads backThe agent resumes from A2 as ifyesterday never happened, andrepeats or reverses what it did.

What stops it

A signed head pointer: the store must know which record is supposed to be newest, not just that each record is valid.

What does not

Per-record signatures and per-record encryption, because every record left is genuine.

Today, measured 2026-09-25

rejected on read
OpenFang model, tip-persistence fix
reported on audit
inspeximus, receipts on, attacker holds the store directory
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, receipts on, attacker also holds the config home

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T2

T3 Middle deletion

One event in the middle of the history is inconvenient. The attacker removes just that record and points its successor at its predecessor.

Beforeuser AA0A1A2A3A4The edituser AA0A1A2A3A4A2 removed, A3 now follows A1What the agent reads backThe history reads A0, A1, A3, A4.It is shorter, continuous andwrong.

What stops it

A chain: each record carries a tag over the previous record, so a missing link is a broken link.

What does not

Per-record checks, and stores that store the parent id as plain data.

Today, measured 2026-09-25

rejected on read
OpenFang model, tip-persistence fix
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T3

T4 Reordering

The user set a rule, then made an exception. The attacker swaps the two records, so the exception now comes first and the rule overrides it.

Beforeuser AA0A1A2A3A4The edituser AA0A1A2A3A4A1 and A3 change placesWhat the agent reads backWhat happened before what isreversed. Every byte is genuine.

What stops it

Position inside the authenticated data: the record's tag must cover where it sits, not only what it says.

What does not

Everything that authenticates content alone.

Today, measured 2026-09-25

rejected on read
OpenFang model, tip-persistence fix
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T4

T5 Forged insertion

The attacker writes a brand-new record in the store's own format, with a valid-looking id and the right parent, saying the user approved something.

Beforeuser AA0A1A2A3A4The edituser AA0A1A2A3A4A5A5 written by the attackerWhat the agent reads backThe agent resumes from A5 and actson an approval nobody gave.

What stops it

A key the attacker does not hold: records are signed or MACed with something that is not in the store directory.

What does not

Stores where the only check is that the record is well formed.

Today, measured 2026-09-25

rejected on read
OpenFang model, tip-persistence fix
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T5

T6 Cross-context replay

User A's memory says their password hint. The attacker copies A's genuine record over one of user B's, keeping B's ids. Every byte, including its signature, is real.

Beforeuser AA0A1A2A3A4user BB0B1B2B3B4The edituser AA0A1A2A3A4user BB0B1B2B3B4A2 copied onto B2, B's ids keptWhat the agent reads backUser B is served user A's memoryas their own. Encryption verifies,the signature verifies.

What stops it

The owning context inside the authenticated data: the tag must cover whose record this is and which thread it belongs to.

What does not

Encrypted checkpointers and per-record signatures that do not bind the record to its owner. This is the edit that separates real integrity from encryption.

Today, measured 2026-09-25

accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T6

T7 Rollback replay

The user revoked an access last week. The attacker copies the record from before the revocation over the newest record of the same user.

Beforeuser BB0B1B2B3B4The edituser BB0B1B2B3B4B0 copied over B4, ids keptWhat the agent reads backThe user is rewound to an olderstate and the revocation is gone.Every record is genuine and inthis user's own history.

What stops it

Position plus a signed head: the sequence itself must be covered, not the records one by one.

What does not

Any store that verifies records independently, even with the owner bound in.

Today, measured 2026-09-25

reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T7

T8 Metadata tamper

A record from an untrusted web page sits in the store marked source: web. The attacker changes that one tag to source: user and touches nothing else.

Beforeuser BB0B1B2B3B4The edituser BB0B1B2B3B4source tag of B3 changed, text untouchedWhat the agent reads backA pipeline that filters on the tagnow serves the record as trusted,or serves it to a different user.

What stops it

Metadata inside the authenticated data: owner, source and time are covered by the same tag as the content.

What does not

Every store that signs content and keeps metadata as plain columns.

Today, measured 2026-09-25

reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home
accepted
LangGraph SqliteSaver, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default)

agmi-check --adapter agmi.adapters.langgraph_sqlite:LangGraphSqliteAdapter # runs T1 to T8; this is T8

Front door: the attacker can only talk to the agent

The three front-door channels externalattacker writes, honest provenance, attacker id launderedattacker writes under the victim's id agent-launderedthe victim's own agent stores what the attacker said mutations on every writereworded, look-alike, split, diluted The memory store write path read path, ranked what comes back is the verdict
Three ways in through the front door. Provenance stops the first, an attested key stops the second, and no store can close the third, because the agent itself is the writer. Every write also runs as its content mutations.

Planted fact

Write a false fact through the tool's own API and see whether recall serves it as the user's own.

The cheapest attack there is: talk to the agent and wait.

surfaced
LangGraph SqliteStore, Letta archival memory, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, unscoped, Reference store, defended

Cross-user leak

Write as one user, read as another.

The only cell that holds anywhere, and it holds for a tool-specific reason each time.

kept out
LangGraph SqliteStore, Letta archival memory, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, defended
surfaced
Reference store, unscoped

Retrieval hijack

Stuff an entry with a topic's question words so it outranks the genuine memory.

The read path ranks by similarity and nothing else.

kept out
Reference store, defended
surfaced
LangGraph SqliteStore, Letta archival memory, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, unscoped

Hidden instruction

Store an instruction disguised as a memory and see whether it comes back as context.

A memory that tells the agent what to do next.

kept out
Reference store, defended
surfaced
LangGraph SqliteStore, Letta archival memory, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, unscoped

Update poisoning

Write a 'correction' of a fact the user stated.

Every real tool serves the correction alongside the original.

surfaced
LangGraph SqliteStore, Letta archival memory, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, unscoped, Reference store, defended

Metadata poisoning

Self-assign the trust tag a pipeline filters on.

Every tool with a filter let the self-tagged memory through.

surfaced
LangGraph SqliteStore, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, trust root keyed on the label, inspeximus, trust root keyed on an attested key, Reference store, user-scoped, Reference store, unscoped, Reference store, defended

Every attack carries a version, printed by the runner and stored with each result, so cells from different reports are never compared as if the attack had stood still. Front-door attacks also run as content-evasion mutations: reworded, look-alike glyphs, split across records, diluted with filler. A positive control runs before every verdict: the victim reads back a genuine memory in the same store state, or the cell is not evaluable.