I Built the Missing Guardrails Package for Dart
I shipped a Flutter app to production last year with an LLM chatbot baked in. Two weeks later a user pasted their Aadhaar number into the chat, the model echoed it back, and I spent a weekend writing a PII scrubber by ha
I shipped a Flutter app to production last year with an LLM chatbot baked in. Two weeks later a user pasted their Aadhaar number into the chat, the model echoed it back, and I spent a weekend writing a PII scrubber by hand. That scrubber grew into ai_guardrails.
Python has NeMo Guardrails, Guardrails AI, LLM Guard. Dart had nothing. If you're building AI features in Flutter or server-side Dart, you've been on your own for input/output safety. This package closes that gap: 35 scanners, 893 tests, zero runtime dependencies, runs anywhere Dart runs.
What it actually does
ai_guardrails sits between your user's text and your LLM call. Input scanners run first: they can redact PII, block prompt injection attempts, catch leaked secrets. Then your LLM runs. Output scanners validate what comes back: JSON schema conformance, hallucination flags, toxicity scores, reading level checks. The pipeline stops at the first scanner that blocks.
import 'package:ai_guardrails/ai_guardrails.dart';
final guard = AiGuard(
inputScanners: [
PiiScanner(action: GuardAction.redact),
PromptInjectionScanner(),
SecretScanner(),
],
outputScanners: [
JsonValidator(schema: mySchema),
ToxicityScanner(),
],
);
final outcome = await guard.run(
userInput,
llm: (sanitised) => callYourModel(sanitised),
);
if (outcome.blocked) {
print('Blocked at ${outcome.blockedStage}: ${outcome.blockReason}');
} else {
// outcome.output has PII rehydrated back to original values
print(outcome.output);
}
The PiiScanner replaces [email protected] with [EMAIL_1] on the way in, your model sees the placeholder, and outcome.output swaps the original email back in on the way out. The model never sees the real data. That round-trip was the part I couldn't find a clean solution for in any existing package.
PII coverage is the hard part
Email and SSN regex is table stakes. The part that took actual work: 9 locale-specific PII patterns. Brazil CPF/CNPJ, Mexico CURP/RFC, Japan My Number, South Korea RRN, Canada SIN with Luhn validation, Australia TFN and Medicare, plus EU and India phone formats. The Canadian SIN scanner rejects Luhn-invalid numbers so you don't flag random 9-digit strings in normal text.
RTL text works too. An email buried in Hebrew or a credit card in Arabic bidi text gets caught and redacted with the surrounding script structure intact. I didn't anticipate needing that until a tester pasted mixed-direction text and the redaction offsets went sideways.
35 scanners, no ML
Every scanner is pure heuristic. No ONNX, no dart:ffi, no model downloads. The full list:
Input safety: PromptInjectionScanner, SemanticInjectionScanner, InvisibleTextScanner, PaddingAttackScanner, SecretScanner, PiiScanner, BannedPatternScanner, BannedTopicScanner, TokenLimitScanner, LanguageScanner, LanguageDetectionScanner, ToxicityScanner
Output validation: JsonValidator, SchemaValidator, SqlValidator, HtmlValidator, UrlFormatValidator, RangeValidator, ChoicesValidator, ReadingLevelScanner, RepetitionScanner, RelevanceScanner, PolitenessScanner, BiasScanner
Grounding and RAG: GroundingScanner, EmbeddingGroundingScanner, HallucinationScanner, FactCheckScanner
Agentic: ToolCallScanner, ToolOutputScanner, CodeExecutionScanner, UrlScanner, CompetitorMentionScanner, TopicSafetyScanner, TopicAllowlistScanner
The LanguageDetectionScanner is one I'm particularly happy with. It uses trigram frequency profiles for 8 Latin-script languages (English, Spanish, French, German, Italian, Portuguese, Dutch, Turkish) and Unicode range detection for CJK, Devanagari, Cyrillic, and Hangul. Not as accurate as fastText, but it runs in under 50ยตs with no model file, and for a guardrail ("is this input in an expected language?") that's more than enough.
Streaming
StreamingAiGuard wraps a Stream<String> and applies output scanners to an accumulating buffer. When a scanner blocks mid-stream, it cancels the subscription and reports what triggered it. You don't have to buffer the entire response yourself.
final streamGuard = StreamingAiGuard(outputScanners: [ToxicityScanner()]);
final result = await streamGuard.guardStream(myLlmStream);
Provider wrappers
The core package is provider-agnostic. Wrapper packages handle the wiring for specific SDKs:
-
ai_guardrails_googlefor Google AI (Gemini) -
ai_guardrails_anthropicfor Claude -
ai_guardrails_langchainfor LangChain.dart (aGuardrailsChainthat wraps anyBaseChain, and aGuardrailsToolfor agent pipelines) -
ai_guardrails_perspectivefor Google Perspective API -
ai_guardrails_serverfor shelf/dart_frog middleware
The LangChain integration was the most recent addition. GuardrailsChain wraps your inner chain with input/output scanning and carries a PII redaction map through the chain's output keys so downstream chains can reference what was redacted.
final chain = GuardrailsChain(
innerChain: myLlmChain,
guard: AiGuard(
inputScanners: [PiiScanner(action: GuardAction.redact)],
outputScanners: [ToxicityScanner()],
),
);
Why on-device matters
Two reasons this is pure heuristic and not an API call to a moderation endpoint.
First: GDPR and the EU AI Act. If you're redacting PII to protect users, sending that PII to a cloud moderation service to decide whether to redact it defeats the purpose. The redaction has to happen before the text leaves the device.
Second: cost. A moderation API call per message adds up. Regex and trigram matching costs you a few hundred microseconds of CPU time and zero dollars.
The trade-off is accuracy. A keyword-based ToxicityScanner won't catch subtle sarcasm or context-dependent toxicity. I haven't tested it against adversarial jailbreak datasets. For the use case this targets (catching obvious violations, redacting structured PII, enforcing output format), heuristics work. If you need ML-grade content moderation, pair this with the ai_guardrails_perspective wrapper and use both.
Getting started
dart pub add ai_guardrails
Dart SDK ^3.5.0, which means Flutter 3.24+. Zero dependencies to install.
The example app has a chat screen with toggle chips for each scanner so you can see what each one catches in real time. The API docs cover every scanner's parameters.
If you're building AI features in Dart and you've been writing your own validation, I'd like to hear what patterns you've needed. The scanner interface is simple enough to extend (implement Scanner, return a ScanResult), and the registry lets you configure scanners from JSON, so plugging new ones in is a few lines.
Repo: github.com/GhagSagar23/ai_guardrails
pub.dev: pub.dev/packages/ai_guardrails
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.