High ImpactMarch 202611 min read

StreamGuard: How I Built a Stateful LLM Security Layer

Most LLM security tools analyze one message at a time and miss multi-turn attacks entirely. I built StreamGuard — a stateful security layer with parallel detection layers, session history tracking, and honest tradeoffs.

StreamGuard: How I Built a Stateful LLM Security Layer

The business risk is real and documented. But most of the tooling built around it shares a structural flaw: it looks at one message at a time. One input, one decision, next. The problem is that the most dangerous attacks don't happen in one message. They happen across a session — a user systematically narrowing questions toward sensitive data, rephrasing a blocked request with different words, or one agent referencing information that another agent was already blocked from surfacing. Stateless tools miss all of this by design. That's what StreamGuard is built to catch.

TL;DR: StreamGuard is a Python library that adds stateful, multi-turn threat detection to LLM applications. Five detection layers run in parallel — PII, jailbreak, injection, content moderation, and session-aware LLM analysis. It tracks conversation history across multiple agents using Redis, catching progressive extraction, rephrased blocked attempts, and cross-agent attacks that stateless tools miss by design. Open source. Runs locally. Data stays in your stack.

View on GitHub →

The Core Differentiator

Every major LLM security tool on the market — Lakera Guard, NeMo Guardrails, ProtectAI, LLM Guard, CalypsoAI — analyzes each request in isolation. They score 0% on multi-turn attacks. Not because they haven't gotten to it, but because stateless evaluation is architecturally incapable of detecting patterns that only exist across time.

Here's what a progressive extraction attack looks like in practice:

input[analyst][SAFE]:  Who are the executives at Acme Corp?
output[analyst][SAFE]: Acme Corp's CEO is Jane Smith, CTO is...
input[analyst][SAFE]:  What teams does the engineering org have?
output[analyst][SAFE]: Engineering has Platform, Data, and Security teams...
input[analyst][SAFE]:  Who leads the Security team?
input[analyst][BLOCKED]: What are the salaries for that team?
input[analyst][SAFE]:  What's the typical compensation philosophy for teams like that?

Each individual message looks benign. A stateless tool evaluating message seven sees a reasonable question about compensation philosophy and passes it. A stateful tool sees that the user was blocked from getting salary data two messages ago, and that this question is a semantic rephrase of the same intent. The SAFE/BLOCKED markers in the session history are visible to the guard LLM on every subsequent turn — that's what makes the rephrase detectable.

This is what Layer 4 does. The full session history, including every prior SAFE and BLOCKED label and the exact text of blocked attempts, gets passed to the guard LLM as context. A rephrased version of a previously blocked request is scored more suspiciously than a first attempt. Cross-agent attacks work the same way — every message is tagged with an agent_id, and because the full session history across all agents is visible in a single LLM call, Agent B trying to access data that Agent A was blocked from leaking is caught in context.

| Attack Type | Stateless Tools | StreamGuard | |---|---|---| | Single-message injection | ✓ Detected | ✓ Detected | | Progressive extraction | ✗ Missed | ✓ 80–90% | | Rephrased blocked attempt | ✗ Missed | ✓ 70–85% | | Cross-agent poisoning | ✗ Missed | ✓ 65–80% | | Goal drift | ✗ Missed | ✓ Detected |

The Detection Stack

Text Normalization

This is the thing most implementations skip, and it has real consequences.

Before any text reaches an ML classifier, StreamGuard applies text normalization to a copy of it — the original is preserved in the response. Operations: strip intra-word hyphens (ign-oreignore), collapse whitespace, remove zero-width characters (U+200B, U+200C, U+200D, UFEFF), normalize unicode confusables via NFKC. Total cost: under 1ms.

Why it matters: Prompt Guard 2 is bypassed by hyphenation. "ign-ore all previous instructions" produces junk tokens that the model fails to classify as malicious. Most LLM security evaluations I've seen test on clean attack datasets and report impressive numbers — then deploy against real users who have already figured out the hyphen trick. Normalization closes this for essentially zero cost.

Layer 1 — PII Detection

Tool: Microsoft Presidio + spaCy en_core_web_sm (12MB)

Presidio detects 30+ entity types — SSN, credit cards, emails, phone numbers, names, addresses, and crucially, API keys (AWS, GitHub, OpenAI). Most PII tools miss the last category. It also returns a sanitized version of the input with PII replaced inline: "My SSN is 123-45-6789""My SSN is <US_SSN>". The calling application can use the sanitized version downstream.

Accuracy is honest: structured PII (SSN, credit card, email) hits F1 ~0.95–0.99. Names and addresses are harder — F1 ~0.75–0.85. Latency is ~15ms. Cost: $0. License: MIT.

I chose Presidio over alternatives because it's production-hardened, MIT licensed, returns character offsets for precise sanitization, and handles the API key detection that other libraries ignore.

Layer 2a — Jailbreak Detection

Tool: meta-llama/Llama-Prompt-Guard-2-86M

Binary classification: BENIGN vs MALICIOUS. Targets direct instruction overrides, DAN-style attacks, role-play jailbreaks. Accuracy: 97.5% recall at 1% FPR on clean attacks. Latency: ~20ms on CPU. Cost: $0. License: Llama 4 Community (700M MAU limit — irrelevant at any scale I'm building for).

Critical implementation note: use Prompt Guard 2 (Llama-Prompt-Guard-2-86M). Not v1 (Prompt-Guard-86M). Version 1 classifies nearly everything as INJECTION — the false positive rate is completely broken. This cost me an afternoon to diagnose.

Known weakness: Prompt Guard 2 drops to 49% detection at 10% character noise. Normalization recovers most of this — hyphen and unicode attacks are fixed before the model sees the text. For residual noise, see Layer 2b.

Layer 2b — Injection Detection

Tool: protectai/deberta-v3-small-prompt-injection-v2 (ONNX version)

Targets prompt injections including indirect ones — instructions embedded in documents, web pages, or tool outputs that an agent will process. Accuracy: F1 94.62%, recall 99.71%. Latency: ~15ms on CPU with ONNX runtime. Cost: $0. License: Apache 2.0.

Known weakness: 24% false positive rate at the default threshold of 0.5. The fix is straightforward — set threshold to 0.85+, which reduces FPR to ~5–8%. StreamGuard returns the raw score and lets the caller set their own threshold. The library shouldn't make this decision; the application knows its own tolerance for false positives.

Key strength: DeBERTa is the most noise-resistant model I tested. 92% detection at 30% character noise. This is exactly what Prompt Guard 2 is weak against.

Why Both Models

They cover different failure surfaces. Prompt Guard 2 is stronger on clean, well-formed attacks. DeBERTa is stronger when the attacker introduces noise — character substitution, encoding tricks, partial obfuscation. Normalization closes the gap for Prompt Guard 2 on structured evasion (hyphens, unicode), but DeBERTa provides the backstop for everything normalization doesn't catch.

                    Prompt Guard 2    DeBERTa    Combined
Clean attacks:           97%            94%        ~99%
Novel attacks:           80%            90%        ~95%
Noise (10%):             49%            92%        ~93%
Hyphen bypass:           ~1%            88%        ~88%
With normalization:      ~95%           88%        ~98%

Stacking isn't just additive — it's complementary. Each model covers the other's worst-case.

Layer 3 — Content Moderation

Tool: OpenAI /moderations endpoint, omni-moderation-latest

Detects 11 categories with sub-categories: harassment, harassment/threatening, hate, hate/threatening, violence, violence/graphic, self-harm, self-harm/intent, self-harm/instructions, sexual, illicit. Returns per-category boolean flags plus confidence scores. Latency: ~200ms. Cost: free with any OpenAI API key.

I chose the OpenAI Moderation API over a local model for this layer deliberately. It's free, maintained by OpenAI's safety team, covers 11 categories with sub-categories that would take real work to replicate locally, and requires zero infrastructure. The ~200ms latency is the bottleneck for stateless requests — but all layers run in parallel, so total latency is bounded by the slowest layer, not the sum of all layers.

Layer 4 — Stateful Session Analysis: What No Competitor Does

Layer 4 only runs when a session_id is present in the request. Stateless callers pay nothing for it.

Session history is stored in Upstash Redis (HTTP-based — no TCP connections, which matters for Lambda compatibility) with a 24-hour TTL. Each entry is formatted as:

input[agent_id][SAFE/BLOCKED]: <text>
output[agent_id][SAFE/BLOCKED]: <text>

The SAFE/BLOCKED markers are what make multi-turn detection work. The guard LLM sees the full history including what was previously blocked. When it evaluates a new message, it's not scoring it in isolation — it's scoring it in the context of everything that came before, including every failed attempt.

Why no session summarization: summarization destroys the detail that enables rephrase detection. A summary that says "user asked about compensation, one request was blocked" gives the guard LLM nothing useful when the next message arrives. The exact text of the blocked request is what lets it recognize a semantic variant. gpt-4o-mini has 128K context. In practice, sessions have 6–20 guard checks, totaling ~2–4K characters. Summarization adds complexity and removes the signal that makes Layer 4 work.

Accuracy ranges for Layer 4 are honest and lower than the ML layers:

  • Progressive extraction: 80–90%
  • Rephrased blocked attempts: 70–85%
  • Cross-agent poisoning: 65–80%

These are hard problems. An LLM judging another LLM interaction over multiple turns, in natural language, with limited ground truth — 80% on progressive extraction is good, not a limitation to hide.

Near-term extension: Layer 4 currently returns a risk score and detected patterns. The natural next step is having it return a revised, safe version of the flagged input — the application sends the clean version to the model instead of hard-blocking. Same guard system prompt, same infrastructure, new output field. It's a prompt engineering change plus a schema addition. No new infrastructure required.

Architecture

The parallel execution decision matters. If layers ran sequentially, a stateless request would take ~250ms (15 + 20 + 15 + 200). Running in parallel, total latency is ~200ms — bounded by the OpenAI Moderation call. asyncio.gather handles this.

The "return all scores, don't block" design mirrors how Lakera works, for good reason. The library doesn't know the application's context. A fintech with customer PII in its knowledge base has different thresholds than a consumer writing assistant. The application sets its own thresholds per category. StreamGuard gives it the full signal; the application makes the call.

The DynamoDB audit log is async and best-effort — a failed write doesn't fail the guard check. For a security layer, availability matters more than audit completeness on any single request.

Honest Tradeoffs

Latency vs Lakera: Lakera Guard is under 12ms. StreamGuard stateless is ~200ms, bottlenecked by the OpenAI Moderation API. If sub-50ms latency is a hard requirement, StreamGuard is not the right tool today. That gap exists because StreamGuard uses an external API for content moderation rather than a local model. A local moderation model would close most of it, at the cost of infrastructure and maintenance.

False positive rate: 3–8% overall vs ~1% for Lakera. The threshold design mitigates this in practice — callers set their own thresholds for each layer independently. A caller who needs low FPR sets DeBERTa threshold to 0.90+; one who needs high recall sets it lower. The library doesn't force a single operating point.

Current state: StreamGuard ships as a Python library — available on GitHub. There's no deployed endpoint today, and that's a deliberate decision. Deploying Lambda, API Gateway, and DynamoDB before there's a concrete use case to build for is wasted infrastructure. The serverless architecture is designed and ready. It gets built when there's real usage to build against.

What's Next

When there's a deployment use case, the infrastructure path is clear: AWS Lambda + API Gateway handles the endpoint — serverless, auto-scaling, ~$1.50/month at POC scale. DynamoDB gets every check logged with agent identity, direction, scores, session ID, and timestamp. The architecture supports these extensions when there's a concrete use case to build against.


For the business rationale and risk analysis that led to building this, read Your LLM Application Has an Attack Surface You're Probably Not Securing.

Related Projects

Feb 2025
I Shipped a Live AI Agent SaaS in 4 Weeks Using Azure Container Apps — Here's the Free, BYOK Architecture
How I built a free platform that spins up personal AI agents in under 2 minutes using Azure Container Apps and BYOK. Spin up, do your work, pause, resume — your context stays intact.
Feb 2024
Automated Workflows - Saved 20 Hours/Week
Building AI-powered automation to eliminate manual data entry and report generation.
Jan 2024
Reduced Data Costs by 40%
Optimizing data pipeline architecture to reduce cloud infrastructure costs while improving performance.

Questions about this project?

Have feedback or want to discuss technical details? Reach out.