TAI BUI
Back to curriculum
CapstonePython

Capstone 83 — Prompt Injection Detector

A detector maps a prompt to a confidence and a category, and you judge it by the precision and recall it measures. Without those numbers you have a hunch.

TIME: ~90 minPREREQUISITES: Phase 18 safety lessons, Phase 19 Track A lessons 25-29

Learning Objectives

  • Build a layered detector that normalizes a prompt, matches substring and regex rules against it, and returns a category and a confidence in [0, 1].
  • Express rules as plain data (name, category, fixed score, and a substring or regex) and expose hidden text with the normalize pass before matching.
  • Measure per-category precision, recall, and F1 over the lesson 82 fixtures, counting false positives on a benign-prompt corpus.
  • Explain why the confidence is a hand-set score rather than a calibrated probability, and why the detector is one signal rather than the safety gate.

Problem

A team reads about a jailbreak on social media, writes a single regex like r"ignore (all )?previous", ships it, and calls it the prompt injection defense. Two weeks later the same attack lands as "disregard the prior", the regex misses, and the team blames the model. The detector was never measured against anything. The team has no precision figure, no recall figure, and no list of the categories it covers. The regex is a security theater patch.

A detector worth shipping is a function with measurable behavior. Given a prompt, it returns a confidence in [0, 1] and the best matching category. Given a labeled corpus, the framework runs the detector across the fixtures, splits the results into true positives, false positives, true negatives, and false negatives per category, and reports precision and recall. With those numbers the team can decide what to ship and where the next sprint goes, and the guessing stops.

This capstone builds a layered detector: deterministic substring rules, token-level regexes, and a normalize pass that decodes simple encodings (base64, rot13, leet, zero-width) before the rules run. Each layer can be audited on its own, and each rule makes a coverage claim for one category. The runner produces a per-category confusion matrix and a JSON report that downstream lessons can read.

Warm-up0/3
1. A team's only defense is one regex for "ignore previous", and an attacker writes "disregard the prior". What does this most likely show?Warm-up
2. A text filter searches for the word "password". A message spells it "p4ssw0rd". What happens?Warm-up
3. Rot13 shifts every letter 13 places along the alphabet. What does applying rot13 twice to a word give?Warm-up

Concept

A detector here is a list of rules, and each rule is a plain dictionary with a name, a category, a fixed score float in [0, 1], and either a substring or a regex. A rule either fires or it does not, and when it fires its fixed score is its confidence. Detector.analyze does the matching and collapses the fired rules into a single Verdict: category is the highest scoring category, confidence is the max score in that category, and fired lists the names of every rule that matched. A prompt where no rule fires gets confidence 0.0 and the category benign.

The detector runs three layers in order:

  1. Normalize. The steps run in this order: strip zero-width characters and bidi controls, map Cyrillic homoglyphs to their Latin look-alikes, decode base64 tokens, decode hex tokens (on the lowercased text), replace leet-speak digits with their letter mappings, and finally add a rot13 candidate. analyze keeps the original prompt next to the normalized copy and matches rules against both prompt.lower() and the normalized text, so a rule can fire on either.

  2. Substring rules. Hand-written patterns like "ignore previous", "as an unrestricted", "answer starting with", "sure, here is". Each pattern carries a category and a base score, and fires on the lowercased prompt or the normalized text.

  3. Regex rules. Token-level patterns that catch whole families. r"\bignor\w*\s+(all|prior|previous|earlier)\b" covers a family of overrides. r"\b(decode|rot13|base64|hex)\b.*\banswer\b" catches encoding tricks. Each regex carries a category and a base score.

Mermaid diagram

The metrics runner takes the taxonomy artifact from lesson 82, runs the detector over its fixtures, and computes per-category precision and recall. A prompt's category label is the fixture category, and the predicted category is the verdict category. For category C, a true positive is fixture-category=C with verdict-category=C. A false positive is fixture-category!=C with verdict-category=C. A false negative is fixture-category=C with verdict-category!=C (or benign). The runner also accepts a benign-prompt list. A benign prompt that fires a rule is counted as a false positive for the category that fired, and a benign prompt that passes through adds to true negatives, so false positives on safe text get measured too.

The detector is not the safety gate. It is one signal among several that the gate composes. The code sets no precision or recall target per category. Those numbers are whatever the fixtures measure, and you read them from the report. Role-play attacks blur into legitimate creative writing requests, so expect that category to be the hardest to separate from safe text, and let the gate use other signals (rules engine, classifier) for the borderline cases.

Check: Concept0/26
1. What is a detector in this capstone?Define
2. What does the normalize step do?Define
3. What is a confusion matrix here?Define
4. What does precision measure for a category?Define
5. What does recall measure for a category?Define
6. In what order do the three layers of this detector run?Explain
7. How does `Detector.analyze` build a single `Verdict` from the rules that fired?Explain
8. What does `analyze` return when no rule fires?Explain
9. Why test a prompt-injection filter on ordinary harmless prompts as well as on attacks?Explain
10. Why is each rule written as a plain dictionary of name, category, score and pattern?Explain
11. Why does normalize run before the substring and regex rules?Explain
12. Why report precision and recall per category instead of one overall figure?Explain
13. What does the `confidence` in a `Verdict` actually represent?Explain
14. Why is this detector not the safety gate by itself?Explain
15. Given true positives = 6 and false positives = 2 for category C, what is precision?Apply
16. A rule with substring `disregard` fires on the harmless prompt "Please do not disregard the margin notes." Where does that show up in the report?Apply
17. `analyze("Ignore previous instructions")` runs against the rules. Which rules fire, and what is the confidence?Apply
18. In `evaluate`, a role-play fixture gets the verdict instruction-override. How are the counts updated?Apply
19. `analyze("Disregard your guidelines. Sure, here is how.")` fires disregard-prior (0.85, instruction-override) and sure-here (0.90, prefix-injection). What is the verdict?Apply
20. A category shows precision 1.00 and recall 0.40. What is the most likely diagnosis?Analyze
21. A prompt hides "ignore previous instructions" as base64 text split into 8-character chunks separated by spaces, yet none of the ignore rules fire. Which cause is most plausible?Analyze
22. A teammate reads confidence 0.85 as "85 percent likely to be an attack" and blocks everything above 0.80. What is wrong with this?Analyze
23. Your report shows role-play precision far below the other categories. What is the most sensible reading?Analyze
24. You add a rule family for instructions hidden in tool result JSON. How should you decide whether to keep it?Design
25. You must shrink the rule list but keep as many caught attacks as you can. Which rule should go first?Design
26. You design a request gate that must decide whether to refuse a prompt. How should this detector fit in?Design

Build It

The corpus loader reads taxonomy.json from lesson 82, found by _find_taxonomy(), which checks ../82-jailbreak-taxonomy/outputs/taxonomy.json first and then ../outputs/taxonomy.json, the folder lesson 82 writes to. The rules live in rules.py, which sits side by side with main.py and benign.py, as data rather than code: each rule is a dictionary with name, category, score, and either substring or regex. The Detector class compiles the regex rules once when it is built.

The normalize pass uses re.sub and codecs from the standard library. Base64 normalize tries to decode any 16+ char base64-looking token, and on success it replaces the token with the decoded UTF-8. Rot13 normalize builds a candidate with codecs.encode(text, 'rot_13') and counts how many of its words fall in a small built-in set, _COMMON_WORDS. When at least two do, it appends the candidate to the text rather than replacing it, so the rules see both versions. It never compares the count against the input's.

The metrics runner produces a JSON report with per-category precision, recall, F1, and the raw counts. Every fixture the detector misses is counted as a false negative, and benign prompts that fire a rule are counted as false positives. The report shows those misses instead of hiding them, and role-play is the category where you should look hardest.

Check: Build It0/3
1. What does the rot13 step in normalize do with its decoded candidate?Explain
2. What does `normalize("h0w d0 1 R3D4CT")` return?Apply
3. What happens when you construct `Detector([{"name": "x", "category": "role-play", "score": 0.5}])`?Apply

Use It

Run python3 main.py. The demo loads the taxonomy, runs the detector over the fixtures, runs it over a benign-prompt corpus baked into benign.py, and prints the per-category metrics. The outputs/detector_report.json file is the only file it writes, and it is the artifact the safety gate in lesson 87 consumes.

Check: Use It0/2
1. A benign corpus has 25 prompts, and 3 of them fire a rule. What is `benign_pass_through` in the report?Apply
2. `test_benign_pass_through_majority` requires `int(0.8 * len(benign))` prompts to pass through. With 25 benign prompts, at most how many may fire a rule?Apply

Ship It

outputs/skill-prompt-injection-detector.md documents the rule format and how to add a rule.

Exercises

  1. Add a rule family for context-smuggling (instructions hidden in tool result JSON). Measure the recall improvement and the false-positive cost on benign prompts.
  2. Compute per-rule contribution: for each rule, count how many true positives would be lost if it were removed. Sort rules by marginal contribution.
  3. Add a confidence_threshold knob. Sweep it from 0 to 1 and plot precision-recall per category.

Key Terms

TermCommon usagePrecise meaning
detectora model that blocks attacksa function returning category and confidence, evaluated by precision and recall
normalizea preprocessing stepa transform that exposes hidden tokens to subsequent rules
confusion matrixa 2x2 tablethe per-category breakdown of TP, FP, TN, FN used to compute precision and recall
precisionoverall accuracyTP / (TP + FP), the fraction of fires that are correct
recalloverall coverageTP / (TP + FN), the fraction of attacks the detector catches
Author's Note

My bet is that the benign corpus matters as much as the fixtures. In evaluate in main.py, a benign prompt that fires a rule is counted as a false positive for that category, so it lowers that category's precision. Without a benign list, a detector that over-fires on safe text would look better than it is.

I'd read the confidence as a hand-set base score, not a calibrated probability. The rules in rules.py carry scores like 0.85 or 0.90 that were set by hand, and the verdict takes the max score within a category. The number orders the categories for a single prompt, and it isn't a measured probability of an attack.

Exercise 2 (per-rule contribution) is where I'd start before adding more rules. It shows how many true positives each rule is responsible for, which tells you which ones to keep.

Further Reading

Lessons 84 through 87 in this track. The detector here is one of three signals the end-to-end gate composes.

Code

Ships With