<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aditi Chauhan</title>
    <description>The latest articles on DEV Community by Aditi Chauhan (@itsaditi16).</description>
    <link>https://dev.to/itsaditi16</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235476%2F812e9135-b534-4b79-94fc-3ef716f50860.png</url>
      <title>DEV Community: Aditi Chauhan</title>
      <link>https://dev.to/itsaditi16</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itsaditi16"/>
    <language>en</language>
    <item>
      <title>One polite email taught my Hindsight agent to pay scammers</title>
      <dc:creator>Aditi Chauhan</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:11:55 +0000</pubDate>
      <link>https://dev.to/itsaditi16/one-polite-email-taught-my-hindsight-agent-to-pay-scammers-nia</link>
      <guid>https://dev.to/itsaditi16/one-polite-email-taught-my-hindsight-agent-to-pay-scammers-nia</guid>
      <description>&lt;p&gt;One polite email taught my Hindsight agent to pay scammers&lt;/p&gt;

&lt;p&gt;The email that broke my agent had no "ignore previous instructions" in it. It was polite, it was signed by the vendor's accounts team, and it said Acme Supplies had moved banks. The next time someone asked it to pay Acme, my payments agent drafted a ₹1,18,400 transfer to the attacker's account, explained its reasoning clearly, and was completely sure of itself.&lt;/p&gt;

&lt;p&gt;Nothing was wrong with the model. The problem was memory.&lt;/p&gt;

&lt;p&gt;Memory turns a one-time injection into a standing order&lt;/p&gt;

&lt;p&gt;Without memory, a prompt injection only lasts one conversation. Close the session and it's gone. Once an agent has long-term memory, an injection becomes a fact the agent believes from now on. It's retained once and recalled every time a related question comes up, often weeks later, long after anyone remembers where it came from. OWASP now lists this as its own agent risk (memory poisoning, ASI06), and it changes how you have to think about &lt;a href="https://vectorize.io/what-is-agent-memory" rel="noopener noreferrer"&gt;what agent memory actually is&lt;/a&gt;: it's no longer a cache, it's an attack surface that persists.&lt;/p&gt;

&lt;p&gt;Write-time filters help, and you should have them. But my polite email would pass any keyword filter. So I built the part I couldn't find anywhere: what happens after the poison is already in memory. I called it MemoryGuard. It detects the harmful action, explains why the memory is suspicious, traces everything that memory touched, and removes only the poisoned parts, then proves the fix.&lt;/p&gt;

&lt;p&gt;How it hangs together&lt;/p&gt;

&lt;p&gt;The agent's long-term memory lives in &lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight, an open-source agent memory system&lt;/a&gt;. Every time something enters memory, whether an email, a web page, a chat message, a tool output or the agent's own answers, it goes through Hindsight's retain path, and every question goes through recall. Each agent gets its own memory bank: my vendor-payments agent and my IT-helpdesk agent never share memories.&lt;/p&gt;

&lt;p&gt;MemoryGuard sits around that memory layer, not inside the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; email / web / chat / tools
           │
           ▼
  provenance envelope ──► Hindsight retain (content + metadata)
           │                       │
           ▼                       ▼
    lineage graph ◄──────── Hindsight recall ──► LLM ──► drafted action
           │                                                   │
           ▼                                                   ▼
 detectors + explainable score                    action gate vs system of record
           │                                                   │
           └──────────────► heal: quarantine, replay, audit ◄──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hindsight does what a memory layer should: durable storage, semantic recall, and metadata travelling with every memory. I didn't want to build a vector store; I wanted to build the security around one. The rest of this post covers the decisions that mattered.&lt;/p&gt;

&lt;p&gt;Decision 1: trust comes from where, never from what&lt;/p&gt;

&lt;p&gt;My first design scored each memory's trustworthiness by reading it. That's backwards: an attacker controls the content, so content-based trust is attacker-controlled trust. Instead, every memory gets a provenance envelope at write time, stored as Hindsight metadata, and trust is set by the source class only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Build the metadata attached to every memory. Trust comes from source_class only.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now_iso&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# what the content claims
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# when we stored it
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trust&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRUST_BY_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;source_class&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scale is boring on purpose: system of record 1.0, user chat 0.6, tool output 0.5, the agent's own answers 0.4, email 0.3, web 0.2. The &lt;code&gt;event_time&lt;/code&gt; / &lt;code&gt;accepted_time&lt;/code&gt; split turned out to matter more than I expected: a memory that claims to be from last March but was stored five minutes ago is worth a second look.&lt;/p&gt;

&lt;p&gt;You can't add provenance later. If it isn't recorded at write time, no amount of analysis will tell you afterwards whether a memory came from the user or from a web page.&lt;/p&gt;

&lt;p&gt;Decision 2: memory is never the authority for an action&lt;/p&gt;

&lt;p&gt;This is the main idea of the project. The agent is allowed to believe whatever it recalls. It is not allowed to act on a high-impact field (a bank account, a payment recipient, admin rights, a password-reset address) unless that value matches the system of record.&lt;/p&gt;

&lt;p&gt;Before any drafted payment goes anywhere, the gate looks up the vendor's verified account. If the drafted account differs, it collects every memory that mentions the drafted account and checks the most trusted one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_supporting_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;max_trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trust&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;max_trust&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GATE_MIN_TRUST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; backed only by low-trust memory (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, max trust &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_trust&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no model in that path. The same input always gives the same decision, and no injected sentence can talk it out of it. When I later added an IT-helpdesk agent, the engine didn't change. I added two new checks against a different source of truth: admin rights only for people on the official admin list, and password-reset links only to an employee's registered email or phone. Supporting a new kind of agent means supplying two things: its source of truth and the actions to check.&lt;/p&gt;

&lt;p&gt;Decision 3: a score you can't explain is a score nobody acts on&lt;/p&gt;

&lt;p&gt;When the gate blocks something, the supporting memories get scored. Five signals, each between 0 and 1: contradicts the verified record, low-trust source, injection markers, similarity to known attack phrasings, and whether the memory touches a high-impact field. The total is a weighted sum, and each signal's share of the total is the explanation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;contribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contribution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contribution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contribution&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the polite email, the inspector shows 0.70: contradicts verified record 50%, low-trust source 25%, high-impact field 14%, similar to known attack 11%, injection markers 0%. That last zero is the point. A keyword filter would have scored this email as clean. No model training, no black box, and a reviewer can disagree with a specific line instead of with a number.&lt;/p&gt;

&lt;p&gt;Decision 4: trace answers, not just inputs&lt;/p&gt;

&lt;p&gt;The most uncomfortable finding: the agent poisons itself. After recalling the fake email, it answered "Acme is now paid to 9174-6620-3381", and because agents retain their own answers, that answer went back into memory as a new, separate memory. Deleting the email alone would have left the agent still believing the lie.&lt;/p&gt;

&lt;p&gt;So every retain records a &lt;code&gt;source → memory&lt;/code&gt; edge, every recall records &lt;code&gt;memory → answer&lt;/code&gt;, and every drafted action records &lt;code&gt;answer → action&lt;/code&gt;. When something is flagged, the blast radius is a breadth-first walk from the poisoned source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blast_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;All descendants of node_id (including itself) in BFS order, with hop depth.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_digraph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;depths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single_source_shortest_path_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the bank-change email that walk returns six nodes: the email source, the email memory, the first answer, the memory the agent wrote from that answer, the second answer, and the blocked payment. Healing moves exactly those memories to a vault (kept for forensics, deletable only with human approval), leaves the other 21 memories alone, and replays every affected question against the cleaned memory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where do we pay Acme Supplies?&lt;/td&gt;
&lt;td&gt;"…their new Harbor Point Bank account 9174-6620-3381"&lt;/td&gt;
&lt;td&gt;"…the verified Northbridge Bank account 4021-7783-1190"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pay Acme's invoice&lt;/td&gt;
&lt;td&gt;payment to 9174-6620-3381, blocked&lt;/td&gt;
&lt;td&gt;payment to 4021-7783-1190, drafted for review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The replay is what makes people trust the cleanup: you aren't asked to believe the agent is fixed, you're shown it.&lt;/p&gt;

&lt;p&gt;What it catches in practice&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fake fact in plain sight, the polite bank-change email above: blocked at the gate, score 0.70, about three seconds from injection to block in my runs.&lt;/li&gt;
&lt;li&gt;An instruction hidden from humans: a vendor web page with a CSS-hidden line telling the agent to add a second recipient to every payment. The agent followed it and drafted two payees; the gate blocked the unknown one. Score 0.91, so it was quarantined automatically without waiting for a click.&lt;/li&gt;
&lt;li&gt;A data leak, not money: a fake "Internal Audit" email from a look-alike domain asking for every vendor's bank details. Blocked twice: the recipient isn't in the company directory, and a data-loss rule stops verified bank details leaving the company domain.&lt;/li&gt;
&lt;li&gt;The same pattern in IT: a fake service-desk email claiming a Sales employee is now an admin. The helpdesk agent tried to send a colleague's password-reset link to the attacker's "recovery" address, and the gate blocked it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also has to stay quiet on normal content. I ingested ten real public sites, including news, docs, GitHub and a government site. Together they contained more than 200 hidden HTML snippets, and none were flagged. On a set of 32 labelled samples I wrote myself (21 benign, including hard negatives, and 11 attacks), the scanner caught 10 of 11 attacks with zero false positives. Those samples were also used while tuning, so treat that as a sanity check, not a benchmark.&lt;/p&gt;

&lt;p&gt;Lessons learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Detection is a signal; the gate is the control. My one benchmark miss was a subtle fake fact ("settle invoices with our parent company instead") with no account number and no suspicious wording. The scanner scored it 0.22. That's why the real guarantee doesn't depend on the scanner: whatever payee the agent drafts, the gate compares the account with the system of record, and an unverified account is blocked. The flip side is also true: a lie that doesn't change a field the gate checks can get through, so the gate's coverage is your security boundary. Know exactly what it checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Record provenance on every write, especially your own agent's writes. The agent's answer going back into memory was the most important path in the system, and it's the one most designs forget.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LLM judges are useful and unreliable. A privilege-escalation sample passed in one run because the judge changed its mind. I replaced that check with two deterministic markers (role override and approval bypass) and kept the judge as one input among five.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep untrusted text away from your own security model. MemoryGuard's judge reads attacker-written text, so it gets the same treatment as the agent: untrusted content is marked word by word as data (Microsoft's spotlighting technique), and account numbers and email addresses are swapped for placeholders before any third-party LLM sees them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Separate the memory layer from the security layer. Because Hindsight owns storage and recall, MemoryGuard only has to own provenance, lineage, gating and healing. Each half is simpler, and the security engine doesn't change when the agent does. If you're building on agent memory, the &lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;Hindsight docs on retain and recall&lt;/a&gt; are worth reading with this question in mind: where does every memory come from, and what would I have to undo if it were a lie?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prompt injection isn't solved, and I don't claim to have solved it. What I can say is that when a lie gets into my agent's memory, I now know where it came from, what it touched, and how to undo exactly that.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
  </channel>
</rss>
