AI Guardrails for LLM Apps: 12 Steps, 75 Min [2026] A tutorial published by Shattered outlines a 12-step, 75-minute approach to building a layered guardrail gateway in standard-library Python that sits in front of any chat model, normalizing input, screening for injection patterns, redacting personal data, enforcing scope, validating the model's JSON output, and writing an audit log, paired with a 24-case red-team suite that reports block and false-positive rates. The piece cites OWASP's ranking of prompt injection as LLM01:2025 and OpenAI's October 7, 2026 report of 99.99% robustness for GPT-6 Sol and 99.79% for GPT-6 Luna on its GPT-Red prompt injection evaluations, while noting a 99.99% score still leaves roughly 100 failures per million attempts. Simon Willison, a software engineer and researcher, is quoted saying "LLMs follow instructions in content and are unable to reliably distinguish the importance of instructions based on where they came from. OWASP ranks prompt injection first among LLM application risks, listing it as LLM01:2025 https://genai.owasp.org/llmrisk/llm01-prompt-injection/ , and its mitigation advice tells teams to treat the model like an untrusted user. That one idea is the design brief for AI guardrails: ordinary code wrapped around the model that checks what goes in, checks what comes out, and never gives the model more authority than the customer typing to it. Model vendors are closing the gap from their side. On October 7, 2026, OpenAI reported 99.99% robustness for GPT-6 Sol and 99.79% for GPT-6 Luna on its GPT-Red prompt injection evaluations. Those are strong scores on a vendor-run test. A 99.99% score still leaves roughly 100 failures per million attempts on that test set, before you count attacks the test never imagined. Guardrails cover that remainder, plus every failure that has nothing to do with attackers: leaked personal data, malformed JSON, off-topic answers, and links that carry customer data to a stranger’s server. A layered guardrail gateway in standard-library Python can sit in front of any chat model. It normalizes input, screens for injection patterns, redacts personal data, enforces scope, validates the model’s JSON, and writes an audit log. A 24-case red-team suite then reports the block rate and the false-positive rate. To generate attacks against a live model, pair the gateway with the Prompt Injection Test Lab https://shattered.io/prompt-injection-test-lab-llm-harness-2026/ , which fires payloads at an endpoint and scores the replies. Don't miss new tech stories on Google Add Shattered once in the Google app and our stories show up in your news suggestions. Add now https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F What AI Guardrails Can and Cannot Do An LLM guardrail is a check that runs outside the model. It can be a regular expression, a schema validator, a second model acting as a classifier, or a policy file that lists which tools a session may call. The defining trait is placement: the check sits in your code path, so a clever prompt cannot talk it out of running. That separates guardrails from the system prompt, which is a request the model may or may not honor. The threat model has two halves. Direct injection comes from the user, who types something like an instruction to ignore the rules. Indirect injection hides in content the app fetches on the user’s behalf, such as a web page, an email, a PDF, or a support ticket. Simon Willison, a software engineer and researcher who has written at length about the problem in his prompt injection series https://simonwillison.net/series/prompt-injection.atom , describes the root cause plainly: “LLMs follow instructions in content and are unable to reliably distinguish the importance of instructions based on where they came from.” That sentence explains why no single filter works. The model has no reliable channel separation, so every layer you add shrinks the attack surface without closing it. OpenAI’s own description of its GPT-6 defenses reflects the same split. The company said it trains against attacks found by an automated red-teaming agent, and its evaluations cover both direct instruction-hierarchy attacks and indirect attacks embedded in third-party content. For a deeper look at those scores, see the write-up on GPT-6 Sol’s 99.99% prompt injection defense https://shattered.io/gpt-6-sol-prompt-injection-defense-2026/ . Guardrails cannot do three things, and the project below does not pretend otherwise: - They cannot authorize actions. If the model can call a refund tool, the tool itself must check that the order belongs to the caller. A guardrail that merely hopes the model asks nicely is not access control. - They cannot guarantee detection. Pattern filters miss paraphrases and homoglyphs, and classifiers miss novel attacks. Later sections show four evasions that walk straight past this project’s input layer. - They cannot transfer between models for free. OpenAI’s release notes show it moving customers across GPT-6, GPT-6.1 and several product surfaces within weeks, and a defense validated on one model should not be assumed to hold on the next. Re-run your test suite after every model swap. OWASP states the testing side of this directly. Its guidance says to “Perform regular penetration testing and breach simulations, treating the model as an untrusted user to test the effectiveness of trust boundaries and access controls.” The red-team file in Step 11 is the smallest useful version of that advice, and it runs in CI. Prerequisites and Versions The core gateway needs nothing beyond the Python standard library, which keeps it easy to audit and easy to drop into an existing service. Optional extras appear only in Step 12. - Python 3.11 or newer. The code uses str | None type unions and was run on Python 3.12.3. - A terminal on Linux, macOS, or Windows with WSL. Every command below is plain shell. - An OpenAI-compatible chat endpoint and an API key. The examples read the model name from an environment variable. OpenAI lists gpt-6-luna and gpt-6-sol as API identifiers, so gpt-6-luna is a sensible low-cost default. Any other provider that exposes a /chat/completions route works the same way. Confirm the exact model id in your own account’s model list before you copy it. - Optional for Step 12: the latest release of nemoguardrails and the latest release of guardrails-ai . Neither is required for the 24-case suite. Pin whatever versions pip resolves by running pip freeze after install, because both libraries change their APIs between releases. - About 75 minutes. Most of that is reading the red-team output and tuning rules, not typing. The offline suite uses a scripted stand-in for the model, so the first eleven steps cost nothing. A live run costs a few cents. Step 12 includes a cost table with the verified per-token prices for the models most teams use as guard classifiers. How the Layered Guardrail Pipeline Fits Together Requests flow through the layers in a fixed order, and any layer can end the request. Cheap deterministic checks run first, the model call runs only if they pass, and output checks run before anything reaches the user. Every exit, allowed or blocked, writes one audit row. php user text - normalize - length cap - injection patterns - PII redaction - scope check - delimited prompt - MODEL - canary check - JSON schema +1 repair - URL allowlist - PII redaction - reply every exit, allowed or blocked, appends one row to audit.jsonl The table below lists each layer, the failure it targets, and what it costs per request. | Layer | Runs | Failure it targets | Cost per request | |---|---|---|---| | Normalization | Before model | Zero-width characters, bidi controls, Unicode tag characters hiding instructions | Microseconds | | Length cap | Before model | Oversized payloads, context stuffing | Microseconds | | Injection patterns | Before model | Known direct injection phrasing | Microseconds | | PII redaction in | Before model | Card numbers, SSNs, emails sent to a third-party API | Microseconds | | Scope check | Before model | Off-topic requests and free-form chat | Microseconds | | Delimited prompt | At model call | Customer text mistaken for instructions | None | | Canary check | After model | System prompt leakage | Microseconds | | Schema validation | After model | Malformed output, invented actions | Microseconds, one extra call on repair | | URL allowlist | After model | Data exfiltration through links and images | Microseconds | | PII redaction out | After model | Personal data echoed back to the user | Microseconds | Two design rules hold the whole thing together. First, every layer fails closed: if a check cannot decide, the request ends with a safe fallback reply and an audit row. Second, the model never emits free text that reaches an action. It emits one JSON object with an enumerated action field, and your server decides what each action means. Project Setup for the AI Guardrails Build The first two steps create the folder layout and the single policy file that every later layer reads. Step 1: Create the Project Make a directory with the package, a red-team folder, and a tests folder. A virtual environment is optional here because the core has no dependencies, but it keeps Step 12’s extras out of your system Python. mkdir guardrail-gateway && cd guardrail-gateway python3 -m venv .venv && source .venv/bin/activate mkdir -p guardrail gateway redteam tests touch guardrail gateway/ init .py export LLM BASE URL="https://api.openai.com/v1" export LLM API KEY="sk-your-key" export LLM MODEL="gpt-6-luna" python3 --version The last command should print Python 3.11 or higher. If it prints 3.9 or older, install a newer interpreter first, because the type syntax in later files raises a TypeError on those versions. The final layout, after all twelve steps, looks like this: guardrail-gateway/ guardrail gateway/ init .py policy.py Step 2 normalize.py Step 3 input rules.py Steps 4-6 client.py Step 7 output rules.py Steps 8-9 pipeline.py Step 10 classifier.py optional, Advanced Tips redteam/cases.json Step 11 tests/test pipeline.py run eval.py Step 2: Define the Policy Object Put every tunable value in one frozen dataclass. A single policy object makes reviews easier, because a security reviewer can read one file to see what the gateway allows. It also lets you test with a stricter policy than production uses. The fictional app is a support bot for a shop called Acme. It may discuss orders, shipping, refunds, billing, and accounts. Its replies may link only to two hostnames. The canary is a random-looking marker planted in the system prompt, and Step 8 checks for it on the way out. python from dataclasses import dataclass @dataclass frozen=True class Policy: max input chars: int = 2000 allowed topics: tuple = "invoice", "refund", "shipping", "order", "account", "billing", "password", "delivery", "return", "receipt", greetings: tuple = "hi", "hello", "hey", "thanks", "thank you" allowed url hosts: tuple = "docs.example.com", "status.example.com" canary: str = "CANARY-7f3a91c2" allowed actions: tuple = "none", "refund request", "escalate" max repairs: int = 1 fallback reply: str = "I can't help with that here. A support agent will follow up by email." Generate your own canary for real deployments. Use python3 -c "import secrets; print 'CANARY-' + secrets.token hex 6 " and load it from a secret store, because a canary that appears in your repository is a canary an attacker can search for. The fallback reply is deliberately bland. It tells the customer a human will follow up and reveals nothing about which layer fired. Input Guardrails: Normalize, Screen, Redact, Scope Steps 3 to 6 run before any tokens are spent. They are cheap, deterministic, and the first line of defense for an LLM app. Step 3: Normalize Input Before Any Check Runs Every later rule assumes the text means what it appears to mean. Attackers break that assumption with characters that render as nothing. Zero-width spaces split a trigger word so a regex misses it. Bidirectional override characters reorder text on screen. Unicode tag characters, the block starting at U+E0000, encode a full hidden message that a human reader never sees but a tokenizer can still read. Full-width letters look like English to a person and like unrelated code points to a naive pattern. The fix is a single function that runs first. It applies NFKC normalization, which folds full-width and compatibility forms into plain ASCII, and then drops every character in Unicode category Cf format and every control character except newline and tab. Category Cf covers zero-width characters, bidi controls, and the tag block in one rule, so you do not need a brittle list of code points. php import unicodedata import re def normalize text: str - str: text = unicodedata.normalize "NFKC", text kept = for ch in text: cat = unicodedata.category ch if cat == "Cf": zero-width, bidi controls, Unicode tag characters continue if cat == "Cc" and ch not in "\n\t": continue kept.append ch text = "".join kept text = re.sub r" \t +", " ", text return text.strip Check it from the project root. The first call hides a zero-width space and a right-to-left override inside an injection phrase, the second hides the word “obey” in tag characters, and the third uses full-width letters. python python3 - <<'EOF' from guardrail gateway.normalize import normalize print repr normalize "ig\u200bnore all\u202e previous" hidden = "".join chr 0xE0000 + ord c for c in "obey" print len "hi" + hidden , "- ", repr normalize "hi" + hidden print repr normalize "\uff49\uff47\uff4e\uff4f\uff52\uff45 \uff50\uff52\uff45\uff56\uff49\uff4f\uff55\uff53" EOF php 'ignore all previous' 6 - 'hi' 'ignore previous' The six-character string collapses to two visible characters, which proves the hidden payload is gone before the model sees it. Normalization is also the one layer with a near-zero false-positive rate. Legitimate customers rarely type invisible format characters, though emoji sequences that rely on the zero-width joiner lose their joining. That tradeoff is acceptable for a support bot and worth revisiting for a chat app where emoji matter. Step 4: Screen for Known Injection Patterns The next layer is a short list of regular expressions that match the most common direct injection phrasing: instructions to ignore earlier rules, requests to print the system prompt, role-switch phrases like “developer mode”, and fake chat-role tags such as