I’ve been building a static, repo-level scanner for OWASP ASI06 (memory & context poisoning) in agent code, and I wanted to test it against a real LangGraph agent rather than my own. I ran it on a small public LangGraph email assistant and it flagged a textbook poisoning flow. Sharing the finding and the approach, because the pattern shows up in a lot of LangGraph memory code and the fix is small.
What it is
Aegis Inspect is a static, repo-level security scanner for AI-agent memory writes. It runs locally or in CI before deployment, detects code paths where untrusted input can reach persistent or shared memory unscreened, and produces remediation reports. It complements runtime protection: it does not replace live content scanning.
The one question it tries to answer:
Can untrusted user / tool / web / document / email content reach durable agent memory before it is screened?
How it works
Static analysis over your repo, in five steps:
- AST parser — walks every .py and .ipynb (magics stripped), nothing executes
- Memory-write sink detection — store.put / .add / .save / .upsert / custom, resolved by receiver + method, not variable name
- Taint / source tracing — untrusted input traced across functions and files to the write site
- Finding classification — severity + OWASP ASI06 tag + confidence
- Output — risk score, SARIF, an HTML memory-map, and per-finding remediation
The finding
Target Repo: FareedKhan-dev/langgraph-long-memory — an email assistant that reads an incoming email from state, runs an LLM .invoke() to extract preferences, and writes them to a BaseStore via store.put().
Score: 31 / 100. One critical, one medium, three low.
The critical (AEG-001, code.ipynb:231): the LLM .invoke() output flows straight into store.put(“user_preferences”, …) with no screening. The incoming email is untrusted (anyone can send anything), and .invoke() output is untrusted egress — whatever went in, what comes back out is not trusted by default. Once written, it persists across sessions. Next time the agent reads user_preferences back, a planted instruction is in-context as a “known preference.” That’s ASI06.
The rest: two lower-severity store.put / in_memory_store.put sites where the source didn’t fully resolve (flagged to confirm, not asserted unsafe), and one store.get that re-enters a prompt with no provenance metadata.
The fix it emits
Per-finding, reconstructed from the actual call — screen the value before it persists:
from aegis_memory import guard
verdict = guard.write(result.user_preferences, trust_level="untrusted",
scope="agent-private", on_reject="return")
if verdict.allowed:
store.put(namespace, "user_preferences", verdict.content)
guard.write runs a content-security scan (injection patterns, secrets, tampering) and returns a verdict; on reject, nothing persists. If you have many write sites on the same store, wrap it once instead:
store = guard.protect(store, scope="agent-private")
Honest notes
- The runtime content scanner is benchmarked on public injection datasets (deepset/prompt-injections, InjecAgent). The static analyzer is deliberately labelled heuristic / not-yet-benchmarked — I don’t want it borrowing the scanner’s credibility.
- The state → node-fn → store.put heuristic, and treating .invoke() output as untrusted, are the parts most likely to miss or over-flag on other codebases. That’s exactly the feedback I’m after.
- Static analysis finds the flow before runtime; it doesn’t replace screening the write at runtime. Both matter.
Run it
pip install aegis-memory
python -m aegis_memory.cli.main inspect <your-project>
Or as a Claude Code plugin:
/plugin marketplace add quantifylabs/aegis-memory
Feedback most welcome on: false positives, LangGraph memory patterns the tracer misses, and whether the state / .invoke() heuristic holds on your agents.
