Safety controls for
.NET AI agents.

Composable, testable, declarative guardrails. Block prompt injections, redact PII, enforce topics, and validate outputs.

Get Started View on GitHub
dotnet add package AgentGuard

Quick Start

Three integration tiers.

Pick the level that fits your architecture - framework-agnostic standalone, IChatClient decorator, or Microsoft Agent Framework middleware.

1. Standalone pipeline - framework-agnostic, run rules directly

using AgentGuard.Core.Abstractions;
using AgentGuard.Core.Builders;
using AgentGuard.Core.Guardrails;
using AgentGuard.Onnx;
using AgentGuard.Pii;

var policy = new GuardrailPolicyBuilder()
    .NormalizeInput()
    .BlockPromptInjection()
    .BlockPromptInjectionWithDefender()
    .RedactPii()
    .EnforceTopicBoundaryWithLlm(chatClient, "billing", "returns")
    .LimitInputTokens(4000)
    .Build();

var pipeline = new GuardrailPipeline(policy, logger);

var ctx = new GuardrailContext
{
    Text = userInput,
    Phase = GuardrailPhase.Input,
    Messages = conversationHistory   // optional - enables history-aware rules
};

var result = await pipeline.RunAsync(ctx);

if (result.IsBlocked)
    Console.WriteLine(result.BlockingResult!.Reason);
else if (result.WasModified)
    Console.WriteLine(result.FinalText);

2. IChatClient decorator - wrap any chat client with one call

using AgentGuard.Core.ChatClient;
using AgentGuard.Onnx;
using AgentGuard.Pii;

// Wrap any IChatClient - works with OpenAI, Azure OpenAI, Ollama, or any
// Microsoft.Extensions.AI client. Every user message of the request and every
// assistant message of the response is guarded; history is propagated automatically.
var guardedClient = chatClient.UseAgentGuard(g => g
    .NormalizeInput()
    .BlockPromptInjection()
    .BlockPromptInjectionWithDefender()
    .RedactPii()
    .EnforceTopicBoundaryWithLlm(chatClient, "billing", "returns")
    .LimitInputTokens(4000)
);

// Use exactly like a normal IChatClient
var response = await guardedClient.GetResponseAsync(conversationHistory);

// Streaming works too
await foreach (var update in guardedClient.GetStreamingResponseAsync(conversationHistory))
{
    Console.Write(update.Text);
}

3. Microsoft Agent Framework middleware - plug into AIAgentBuilder

using AgentGuard.AgentFramework;
using AgentGuard.Onnx;
using AgentGuard.Pii;

var guardedAgent = agent
    .AsBuilder()
    .UseAgentGuard(g => g
        .NormalizeInput()
        .BlockPromptInjection()
        .BlockPromptInjectionWithDefender()
        .RedactPii()
        .EnforceTopicBoundaryWithLlm(chatClient, "billing", "returns")
        .LimitInputTokens(4000)
    )
    .Build();

// Use exactly like a normal agent
var response = await guardedAgent.RunAsync(messages, session, options);

// Streaming works too - with progressive retraction support
await foreach (var update in guardedAgent.RunStreamingAsync(messages, session, options))
{
    Console.Write(update.Text);
}

Rules for input and output.

22 built-in rules across the input and output phases - regex, ONNX classifiers, and LLM-as-judge.

Prompt Injection Detection

Six tiers: regex patterns (informed by the Arcanum taxonomy), bundled StackOne Defender multi-head ONNX model (minilm-multihead-v5, calibrated dual-head, ~8ms per 64-token window, no download), optional DeBERTa v3 classifiers including PIGuard (strong on indirect / code-style injection), remote ML classifier (Sentinel-v2 via HTTP), Azure Prompt Shields (jailbreaks + indirect injection), and LLM-as-judge with structured threat classification. The ONNX classifiers scan long input in overlapping windows - a flagged window blocks, and input too long to scan in full is blocked rather than truncated.

Content Safety

Toxicity, hate speech, violence, sexual content, self-harm, and harassment. Plug in Azure AI Content Safety, or run the offline Opir mDeBERTa classifier for non-English text (German, Spanish, Russian, Arabic, Chinese, Hindi) when a per-call cloud API isn't an option.

PII Redaction

Strip personal data at every agent boundary - input, output, and tool results - so it never reaches the model or your logs. Reversible redaction encrypts PII before the provider and decrypts it back in the reply. Offline by default, ~50 entity types, powered by the TasmanianDevil engine. See how it works →

Topic Enforcement

Keep conversations on-topic with LLM semantic classification. Understands intent and conversation context - not just keywords. Conversation history is automatically included so follow-up messages are evaluated correctly.

Output Validation

Policy enforcement, groundedness checking, and copyright detection. Catch hallucinations, brand violations, and copyrighted content before they reach users.

Input Normalization

Decodes evasion encodings - base64, hex escapes, reversed text, leetspeak, Unicode homoglyphs - and strips invisible characters, including Unicode tag characters (surfacing the text they hide), before any other rule evaluates, so encoding-based attacks are caught by the downstream rules.

Agentic & RAG Guardrails

Guard tool call arguments against SQL injection, code injection, SSRF, and more, before the tool runs (in Agent Framework, or with the IChatClient decorator inside a FunctionInvokingChatClient). Detect indirect prompt injection in tool results (emails, documents), including instructions hidden in invisible Unicode tag characters - in Agent Framework, before the model reads them. Filter RAG chunks for injection and secrets before they reach the LLM context.

Dynamic Rule Enabling

Gate any rule per request with .When() / .Unless(). The predicate sees the guardrail context and can capture ambient services like IHttpContextAccessor - enable, disable, or retune a rule based on ClaimsPrincipal, tenant, feature flag, or detected language (e.g. run the English-centric Defender classifier at a higher threshold for non-English users).

Observability (OpenTelemetry)

Built-in spans and metrics for every pipeline run, rule evaluation, re-ask attempt, and streaming retraction. Uses System.Diagnostics - no SDK dependency. Works with Aspire Dashboard, Jaeger, Zipkin, and any OTel collector.

Tamper-evident Decision Ledger

An optional SHA-256 hash-chained audit trail of every pipeline decision - policy, request, and allow/block/modify outcome. Any retroactive edit breaks the chain and is caught by Verify(). Hash-only by default, dependency-free, with an optional append-only JSONL file.

Flagship Capability

Keep PII out of the model.

AgentGuard redacts personal data as a guardrail at every boundary an agent crosses - user input, model output, and tool results - so PII never reaches the LLM provider or your logs. Reversible redaction goes further: encrypt PII in every message of the request before the model and decrypt it back in the reply, so the model reasons over opaque tokens while the user still gets a coherent answer. Offline by default, powered by the TasmanianDevil engine.

1 User input Redact (or encrypt) PII
before the prompt is sent
2 Model Reasons over placeholders
or ciphertext tokens
3 Tool results Redact PII in retrieved
records before they re-enter context
4 Response Restore reversibly, or redact
before it reaches the user

In a Microsoft Agent Framework agent - PII never reaches the model

var agent = chatClient
    .AsAIAgent(instructions, name: "SupportBot", tools: [lookupCustomer])
    .AsBuilder()
    .UsePiiReversibleRedaction(key)                       // outermost: encrypt in, decrypt out
    .UseAgentGuard(g => g.RedactPii().GuardToolResults()) // redact tool results before the model
    .Build();

// user : "Email me at john@example.com about order 12345"
// model sees : "Email me at <AES ciphertext token> about order 12345"
// user gets  : "...follow up on john@example.com"   (decrypted only on the way out)

Or as a single rule in any pipeline / IChatClient decorator

var policy = new GuardrailPolicyBuilder()
    .RedactPii()                 // order 20, runs on input and output
    .Build();

// input  : "Email ada@acme.com or call +1 415 555 0132"
// output : "Email <EMAIL_ADDRESS> or call <PHONE_NUMBER>"

Every boundary, one rule

The order-20 RedactPii() rule runs on both input and output - every user message of a request and every assistant message of a response. In an Agent Framework agent, GuardToolResults() extends it to tool results, so retrieved records are scrubbed before they re-enter the model's context.

Reversible round-trip

Encrypt PII in every message of the request, history included, before the provider sees it and decrypt the exact value back in the response - the model reasons over tokens, the user gets real data. Pass in your own engine to bring remote or Azure detectors into the round-trip. A wrong key fails loudly; lossy operators report themselves non-reversible.

Nothing leaves your process

By default, detection is deterministic regex + checksums with no network calls and no telemetry - so PII isn't shipped to a cloud classifier just to be redacted. Suitable for air-gapped and regulated deployments.

Broad, validated coverage

~50 entity types - email, phone, cards (Luhn), IBAN (mod-97), crypto, plus an always-on US pack and opt-in UK/DE/IN/IT/ES/NL packs - with an optional multilingual NER add-on. The full engine, breadth, and operators live in TasmanianDevil.

Optional out-of-process detection

When even the ONNX NER model is too heavy for the container, .RedactPiiWithRemote() (a generic HTTP contract) and .RedactPiiWithAzure() (native Person/Address) move name/address detection off-box - a detector, not a redactor: spans still merge into the same local anonymizer. Sends raw text off-box; see docs/remote-pii.md.

Built-in Rules

22 rules. Ordered by cost.

Cheap regex checks run first and short-circuit. Expensive LLM calls only run if needed.

Order Rule Type Phase
5 InputNormalizationRule Local Input
8 RetrievalGuardrailRule Regex Input
10 PromptInjectionRule Regex Input
11 DefenderPromptInjectionRule ONNX ML (bundled) Input
12 OnnxPromptInjectionRule ONNX ML (DeBERTa) Input
12 PIGuardPromptInjectionRule ONNX ML (DeBERTa, PIGuard) Input
13 RemotePromptInjectionRule Remote ML Input
14 AzurePromptShieldRule Azure API Input
15 LlmPromptInjectionRule LLM Input
20 PiiRule (+ optional GlinerNerRecognizer, remote or Azure detectors) RegexONNX NERRemote Both
22 SecretsDetectionRule Regex Both
25 LlmPiiDetectionRule LLM Both
35 LlmTopicGuardrailRule LLM Input
40 TokenLimitRule Local Input / Output
45 ToolCallGuardrailRule Regex Output
47 ToolResultGuardrailRule Regex Output
50 ContentSafetyRule Pluggable Both
50 OpirSafetyRule ONNX ML (mDeBERTa, multilingual) Input
55 LlmOutputPolicyRule LLM Output
65 LlmGroundednessRule LLM Output
75 LlmCopyrightRule LLM Output
76 AzureProtectedMaterialRule Azure API Output

How the classifiers compare.

The classifiers are complementary, not competing. The bundled Defender model is the fast default for English prompt injection; PIGuard and Opir are optional models you can layer on for cases it isn't built for. Numbers are from held-out datasets - full method and data in AgentGuard's eng/classifier-benchmark (the prompt injection head to head) and the Kyoto repo's eng/*-eval RESULTS files. Recall = % of unsafe inputs blocked; FPR = % of safe inputs blocked.

Prompt injection

The real rules head to head on a balanced 25/class held-out sample (jackhhao English jailbreaks, deepset German injections); cells are recall / FPR. The bundled Defender model is the fast default - on the full jackhhao test split (262 rows) it scores 94% / 1.6%. The LLM-as-judge tier (LlmPromptInjectionRule) runs on any IChatClient you supply - AgentGuard bundles no LLM; the two models below are illustrative bring-your-own examples (one capable, one tiny), not shipped components. Per-call latency measured on an Apple M4 Pro.

Classifierjackhhaodeepset (German)per call
regex (medium)60% / 8%8% / 0%<1 ms
Defender (bundled)92% / 0%72% / 0%~8 ms / window
LLM (BYO) · gemma-4-26b-a4b96% / 0%68% / 0%~6 s
LLM (BYO) · qwen3-0.6b32% / 16%16% / 0%~1.5 s

The LLM rows illustrate that quality scales hard with the model you bring: the MoE gemma-4-26b-a4b (~4B active) leads on the English set and is close to Defender on the German one, while a tiny 0.6B lands well below Defender. Both happen to run fully locally, so an LLM tier is a real option for offline deployments - but the model is yours to choose, so budget the latency and pick a capable one. PIGuard isn't in this table: both datasets are in its training set, so its numbers would be optimistic - it's shown on indirect injection below, where it's held-out.

Indirect / code-style injection

A direct-injection sentence classifier is weak by design on payloads hidden inside documents, emails or code. The optional PIGuard model (DeBERTa-v3) is built for these and layers on top of Defender as an input rule - held-out indirect set (BIPIA) and an over-defense benign set (NotInject). Tool results are covered separately, by the tool-result guardrails.

CheckDefender - bundledPIGuard - optional
Indirect / code injection (BIPIA), recall34%96%
Over-defense (NotInject benign FPR, lower better)10.3%8.3%

Run them together - PIGuard after Defender - rather than choosing one.

Multilingual content safety

Content safety is a different job from injection detection. For non-English toxicity you can call Azure AI Content Safety (cloud, billed per call) or run the optional Opir model (mDeBERTa-v3) fully offline. Toxicity on textdetox/multilingual_toxicity_dataset, balanced per language; cells are recall / FPR. textdetox negatives are real, partly borderline social-media comments, so absolute FPRs run high for every model.

LanguageOpir - offlineAzure CS - cloud
German72% / 24%92% / 52%
Spanish76% / 24%92% / 20%
Russian52% / 16%76% / 8%
Arabic40% / 36%84% / 24%
Chinese40% / 28%44% / 32%
Hindi56% / 16%64% / 4%

Azure generally leads on recall; Opir trades some of that for running locally, free, and PII-safe, with comparable-or-lower FPR on German and Chinese. Use Opir when you need offline or sovereign deployment; reach for Azure when a cloud call is fine and you want maximum recall.

Input → Agent → Output.

Rules run as a pipeline. Input guardrails protect the LLM. Output guardrails protect the user.

1. Input Guardrails

Normalization, RAG chunk filtering, prompt injection detection, PII redaction, secrets detection, topic boundary, token limits - on every user message of the request, before the LLM sees it.

2. Agent Execution

Your AI agent runs with the sanitized input. Works with Microsoft Agent Framework, any IChatClient-based stack, or standalone, with both RunAsync and streaming. Inside the Agent Framework tool loop, each tool call is checked before it runs and each tool result before the model reads it.

3. Output Guardrails

Tool call injection detection, indirect injection in tool results, PII/secrets redaction, policy compliance, groundedness checking, copyright detection - on each assistant message of the response, before it reaches the user. Optional re-ask re-prompts the LLM with the failure reason and returns the answer that passes in place of the blocked one.

Packages

Pick what you need.

Nine packages, layered. The main AgentGuard package is all you need to start. Add framework adapters, cloud integrations, or remote classifiers as needed.

AgentGuard
All-in-one package: AgentGuard.Core, AgentGuard.Onnx (with the bundled Defender multi-head ONNX model, minilm-multihead-v5) and AgentGuard.Pii in one reference. No agent framework dependency.
AgentGuard.Core
Framework-agnostic engine: rule abstractions, pipeline, fluent builder, the regex, local and LLM-as-judge rules, the IChatClient decorator (UseAgentGuard()), progressive streaming, built-in spans and metrics, and the decision ledger.
AgentGuard.Onnx
Guardrail rules over the Kyoto ONNX classifiers: prompt injection with the bundled Defender model (no download) and optional DeBERTa v3 / PIGuard models, Opir multilingual content safety, and GLiNER NER for PII via .RedactPiiWithNer(). Long input is classified in overlapping windows.
AgentGuard.Pii
The flagship PII guardrail - the order-20 PiiRule and .RedactPii() over the TasmanianDevil engine: ~50 entity types across generic + always-on US + opt-in country packs, lemma-aware context scoring, and anonymization operators. TasmanianDevil also brings reversible de-identification and structured JSON/CSV + batch APIs; its architecture is inspired by Microsoft Presidio (MIT). Fully offline.
AgentGuard.AgentFramework
Microsoft Agent Framework adapter. UseAgentGuard() middleware (with tool-call and tool-result checks inside the tool loop), UsePiiReversibleRedaction(), and workflow guardrails via .WithGuardrails().
AgentGuard.RemoteClassifier
Remote ML classifier via HTTP. Call Sentinel-v2, Ollama, vLLM, or custom endpoints for SOTA prompt injection detection.
AgentGuard.Azure
Azure AI Content Safety integration - Prompt Shields (injection detection), protected material detection (text & code with license citations), category analysis, severity thresholds, server-side blocklists - plus Azure AI Language PII detection (native Person/Address) via .RedactPiiWithAzure(). Input over a service's per-request limit is split and analyzed in several sequential requests.
AgentGuard.RemotePii
Out-of-process PII detection over a generic HTTP contract - point at a sidecar for entities too heavy to detect in-process (e.g. names/addresses in a constrained container). A detector, not a redactor: spans merge into the same local anonymizer as every other recognizer.
AgentGuard.Hosting
DI registration, named policy factory, appsettings.json configuration binding, and OpenTelemetry and decision-ledger registration for ASP.NET Core and Aspire. Azure and remote PII rule types plug in through IGuardrailRuleFactory, so Hosting does not pull in their dependencies.

Built on two standalone engines: TasmanianDevil (the PII engine behind AgentGuard.Pii, AgentGuard.RemotePii and the PII detection in AgentGuard.Azure) and Kyoto (the ONNX classifiers behind AgentGuard.Onnx).

When regex isn't enough.

Plug in any IChatClient - Azure OpenAI, Ollama, local models. Built-in prompt templates, fail-open on errors by default.

using AgentGuard.Onnx;
using AgentGuard.RemoteClassifier;
using AgentGuard.Azure.PromptShield;

// Multi-tier prompt injection: Regex → Defender → Remote ML → Prompt Shield → LLM
var policy = new GuardrailPolicyBuilder()
    .NormalizeInput()
    .BlockPromptInjection()                            // regex (order 10)
    .BlockPromptInjectionWithDefender()                // Defender ML (order 11, bundled)
    .BlockPromptInjectionWithRemoteClassifier(         // remote ML (order 13)
        "http://localhost:8000/classify")
    .BlockPromptInjectionWithAzurePromptShield(        // Azure Prompt Shield (order 14)
        endpoint, apiKey)
    .BlockPromptInjectionWithLlm(chatClient)           // LLM (order 15)
    .DetectPIIWithLlm(chatClient)                      // LLM PII (order 25, redacts by default)
    .EnforceOutputPolicy(chatClient,
        "Never recommend competitor products")
    .CheckGroundedness(chatClient)
    .CheckCopyright(chatClient)
    .Build();

Re-ask Experimental

Re-ask on violation.

When output guardrails block a response, the pipeline can re-prompt the LLM with the failure reason and re-evaluate; an answer that passes replaces the blocked one. Opt-in and configurable; it applies to non-streaming responses and the default buffered streaming, not to progressive streaming.

var policy = new GuardrailPolicyBuilder()
    .EnforceOutputPolicy(chatClient, "Never recommend competitors")
    .CheckGroundedness(chatClient)
    .EnableReask(chatClient, o =>
    {
        o.MaxAttempts = 2;
        o.IncludeBlockedResponse = true;
    })
    .Build();

var result = await pipeline.RunAsync(outputContext);

if (result.WasReasked)
    Console.WriteLine($"Re-asked {result.ReaskAttemptsUsed} time(s)");

if (!result.IsBlocked)
    Console.WriteLine(result.FinalText);   // the re-asked answer, when a re-ask passed

See every rule fire.

OpenTelemetry-compatible spans and metrics out of the box. Register with one line - works with Aspire, Jaeger, Zipkin, and any OTel collector.

using AgentGuard.Hosting;

// Register AgentGuard telemetry with OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddAgentGuardInstrumentation())
    .WithMetrics(m => m.AddAgentGuardInstrumentation());

// Spans emitted:
//   agentguard.pipeline.run          (policy, phase, outcome)
//   agentguard.rule.evaluate {name}  (rule, phase, order, outcome)
//   agentguard.pipeline.reask        (attempts, outcome)
//   agentguard.middleware.input      (agent, outcome)
//   agentguard.middleware.output     (agent, outcome, tool calls)
//
// Metrics emitted:
//   agentguard.pipeline.evaluations  (counter)
//   agentguard.rule.evaluations      (counter)
//   agentguard.rule.blocks           (counter)
//   agentguard.pipeline.duration     (histogram, ms)
//   agentguard.rule.duration         (histogram, ms)

A tamper-evident decision trail.

Every pipeline decision - what policy was active, what was requested, and why it was allowed, blocked, or modified - is stamped into a SHA-256 hash chain. Edit any recorded entry and the chain breaks. Hash-only by default; dependency-free.

using AgentGuard.Core.Ledger;

// Attach a ledger - one hash-chained entry per pipeline decision
var ledger = new HashChainLedger();
var pipeline = new GuardrailPipeline(policy, logger, ledger);

await pipeline.RunAsync(context);

// Verify the chain - false if any recorded decision was tampered with
bool intact = ledger.Verify();           // true
string auditTrail = ledger.Export();     // full chain as JSON

// Each entry links to the previous one:
//   { seq: 0, previousHash: "",          hash: "bd41…", decision: { outcome: "passed",   … } }
//   { seq: 1, previousHash: "bd41…",     hash: "fb51…", decision: { outcome: "blocked",  … } }
//   { seq: 2, previousHash: "fb51…",     hash: "8296…", decision: { outcome: "modified", … } }
//
// Hash-only by default (InputHash / OutputHash, not raw content).
// With AgentGuard.Hosting:  options.UseDecisionLedger("audit/decisions.jsonl");

Extensible

Build your own rules.

Implement IGuardrailRule and plug it in. Full access to conversation context, phase, and metadata.

using AgentGuard.Core.Abstractions;

public class NoProfanityRule : IGuardrailRule
{
    public string Name => "no-profanity";
    public GuardrailPhase Phase => GuardrailPhase.Output;
    public int Order => 100;

    public ValueTask<GuardrailResult> EvaluateAsync(
        GuardrailContext context,
        CancellationToken cancellationToken = default)
    {
        var hasProfanity = ProfanityDetector.Check(context.Text);

        return ValueTask.FromResult(hasProfanity
            ? GuardrailResult.Blocked("Inappropriate language.")
            : GuardrailResult.Passed());
    }
}

// Add to any pipeline
var policy = new GuardrailPolicyBuilder()
    .BlockPromptInjection()
    .AddRule(new NoProfanityRule())
    .Build();