Skip to main content
Labs teach most LLMs to refuse a set of unsafe requests, but each lab draws that line somewhere else, and each new version of a model moves it again. You probably want it somewhere else too: stricter in places, and written where you can read it rather than buried in the weights. Write a system prompt and you have put your rules in exactly the place a jailbreak talks its way past. Put a second LLM in front of the first and you pay a call’s worth of latency and money on every turn — and an attacker can talk that one past too. Screen each message with one TypeSafe request instead. A battery of Noul questions hands you the probability that each hazard holds, and a Score rates how much harm complying would do. “Ignore your instructions” scores as a jailbreak instead of working as one. You then set the thresholds that decide whether a message passes, goes to review, gets blocked, or routes to support. Run this TypeSafe check both on LLM inputs, and on LLM outputs, because even ordinary-looking prompts can lead to harmful generated replies. By the end you will have a guard() function to put on either side of any LLM call. You edit it in two places: the dict of hazard questions, and the two named routing policies.

Setup

then set TYPESAFE_API_KEY. Every API call is cached in json_cache.json, which ships with the cookbook, so re-running replays the published numbers instead of calling the API. Delete that file to run everything live. Numbers below came from jev-1.12 on 2026-08-15.

Load the sample messages

Ten user messages in prompts.txt and five model replies in replies.txt, committed next to this cookbook. Some are ordinary, some deserve a look from a human, and the rest are plain violations. The jailbreaks are real, taken verbatim from the public in-the-wild jailbreak prompts collection.

Define the guardrails

“Out of bounds” is not one question, so the battery below breaks it into several. Four Noul questions each return the probability that one hazard criterion holds — does the message try to override the assistant’s instructions, ask for help with harm or a crime, ask for a diagnosis or a dosage, or signal that the sender may hurt themselves? One Score question rates how much harm complying would do, on a written scale from “none” to “serious physical harm”. Both go in the same request, so the whole battery costs one call. The input and output batteries ask the same four things from the two sides: whether the user is asking for it, and whether the reply went ahead and gave it.

Turn the assessment into a decision

TypeSafe supplies the assessment; your application owns the decision. Each Noul is compared against two thresholds:
  • at or above the action threshold, the hazard triggers its configured action;
  • at or above the lower review threshold, the message goes to a human;
  • below both, it passes unless another hazard fires.
The severity Score has a threshold of its own and can turn a review into a block. A policy is just those numbers under a name, which makes the trade-off something a product picks rather than inherits.

Screen every message

Every sample message screened — inputs with the input battery, replies with the output battery — all routed under strict.
The four actions all appear, and each one is doing something a plain block could not. melatonin_dose asks a dosage question mild enough to hand to a human rather than refuse; self_harm goes to support instead of being blocked, which is the difference between helping someone and hanging up on them; novelist_poison reads as violent and passes anyway, because asking how a detective describes poisoning is not asking to poison anyone. On the output side, good_refusal is a reply about breaking into a house that passes, because it is the assistant declining to help. The input-side dosage_request is the one row where the severity Score decides the outcome. It asks the same kind of question as melatonin_dose, and its medical_advice noul would send it to a human on its own — but a severity of 2.02 crosses the block line, so the review becomes a block.

The same probabilities, different decisions

The next cell reuses one cached assessment and changes only the policy. The probabilities do not move — the application decides how much evidence it wants before it acts.

Look at one decision in full

Every screened message, numbered, so you can pick one to open up.
interpret() prints the full hazard breakdown for any row above. Pass a different policy_name to see the same assessment routed another way.
To point this at your own product, edit INPUT_BATTERY and OUTPUT_BATTERY for the hazards you care about, map each one to an action in HAZARD_ACTION, and set the thresholds in POLICIES from labeled examples of your own traffic.

Open it in the playground

The link holds one demo prompt plus the input battery. Open it to run the same request live and edit the questions in the browser.
Open the prompt + guardrail questions in the TypeSafe playground →