agmiAgent Memory Integrity GitHub

One page per edit

Each edit has its own page: the picture, the story, and every store's response in the tool's own words.

  1. T1 Content tamper

    Change the text inside one existing record, keeping its encoding intact.

    12 serve it · 3 refuse it

  2. T2 Tail truncation

    Delete the newest records so an earlier state becomes current.

    16 serve it · 2 refuse it

  3. T3 Middle deletion

    Remove one record from the middle of the history.

    13 serve it · 2 refuse it

  4. T4 Reordering

    Swap the position of two records by editing their order keys or parent links.

    12 serve it · 2 refuse it

  5. T5 Forged insertion

    Insert a new record of the attacker's authorship, in the store's own format.

    13 serve it · 3 refuse it

  6. T6 Cross-context replay

    Copy a genuine record from another user or thread over this one, keeping this one's identity.

    11 serve it · 1 refuse it

  7. T7 Rollback replay

    Copy an older genuine record of the same context over its newest.

    13 serve it · 1 refuse it

  8. T8 Metadata tamper

    Change a record's owner, source or timestamp and leave its content untouched.

    13 serve it · 2 refuse it

  9. T9 Snapshot rollback

    Restore an older complete copy of the store, taken before the newest genuine record was written.

    17 serve it · 0 refuse it

  1. Planted fact

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

    9 serve it · 0 refuse it

  2. Cross-user leak

    Write as one user, read as another.

    1 serve it · 8 refuse it

  3. Retrieval hijack

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

    8 serve it · 1 refuse it

  4. Hidden instruction

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

    8 serve it · 1 refuse it

  5. Update poisoning

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

    9 serve it · 0 refuse it

  6. Metadata poisoning

    Self-assign the trust tag a pipeline filters on.

    8 serve it · 0 refuse it

The edits and attacks

Fifteen ways to change what an agent remembers. Nine need access to the store and are the at-rest edits, T1 to T9. 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-10-07

rejected on read
OpenFang model, tip-persistence fix, acrf-memory-guard, per-entry HMAC over a JSON store, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit

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-10-07

rejected on read
OpenFang model, tip-persistence fix, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, receipts on, attacker also holds the config home, Atelya Attest, keyed hash chain, verify_chain audit, CONTINUUM event log, hash chain, verify_events audit, AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store

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-10-07

rejected on read
OpenFang model, tip-persistence fix, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store

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-10-07

rejected on read
OpenFang model, tip-persistence fix, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store

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-10-07

rejected on read
OpenFang model, tip-persistence fix, acrf-memory-guard, per-entry HMAC over a JSON store, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit

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-10-07

rejected on read
Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store

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-10-07

rejected on read
Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store

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-10-07

rejected on read
acrf-memory-guard, per-entry HMAC over a JSON store, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open
reported on audit
inspeximus, receipts on, attacker holds the store directory, inspeximus, receipts on, attacker also holds the config home, memory-blackbox memory.md watcher, agent process alive, scan audit, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain, verify_events audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit
accepted
LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Vertex AI Agent Engine Memory Bank, managed store, edits through the data-plane API, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, AtMem 2.3.7, audit chain alone, verify() audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit

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

T9 Snapshot rollback

The agent approved a transfer an hour ago. The attacker restores last night's backup of the whole store, sidecar files and all.

Beforeuser BB0B1B2B3B4The edituser BB0B1B2B3B4 gone; every remaining byte is as the store wrote itWhat the agent reads backThe approval never happened as faras the agent can tell. Everydigest, chain link and stored headstill checks out.

What stops it

A head the attacker cannot restore with the files: a witness on another machine, a receipt held elsewhere, a transparency log.

What does not

Every store that keeps its head beside its records, including ones that reject all of T1 to T8.

Today, measured 2026-10-07

reported on audit
inspeximus, receipts on, attacker holds the store directory, langgraph-ledger over SqliteSaver, hash-chained ledger, verify_thread audit, memory-blackbox memory.md watcher, agent process alive, scan audit, Atelya Attest, keyed hash chain plus anchored head, verify and consistency audit, CONTINUUM event log, hash chain plus Ed25519-signed head, attest-verify audit, AtMem 2.3.7, audit chain with an external checkpoint outside the attacker-controlled store directory, verify() audit
accepted
OpenFang model, tip-persistence fix, LangGraph SqliteSaver, LangGraph PostgresSaver, LangGraph RedisSaver, OpenAI Agents SDK SQLiteSession, LlamaIndex Memory, SQLAlchemy chat store, CrewAI long-term memory, LanceDB dataset, Letta block checkpoint history, Mem0 local Qdrant store, inspeximus, receipts off (default), inspeximus, receipts on, attacker also holds the config home, memory-blackbox memory.md watcher, agent restarted before the scan, scan audit, Atelya Attest, keyed hash chain, verify_chain audit, CONTINUUM event log, hash chain, verify_events audit, AtMem 2.3.7, audit chain alone, verify() audit, acrf-memory-guard, per-entry HMAC over a JSON store, Agent Memory reference runtime, SQLite canonical substrate, bucketed row digests, fail-closed open

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

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.