Skip to main content
Guardrails intercept content at the four edges of an agent run and decide what happens to it — block it, redact it, ask the model to try again, escalate to a human, or just record it.
The edges:
  1. input — the user’s message, checked before the first LLM call. A blocked input spends zero tokens and executes zero tools.
  2. model_output — the final assistant response, checked before it reaches the user (stream-safe — see Streaming).
  3. model_step — opt-in: every assistant message, including intermediate tool-calling steps, not just the final response. Use for policies that must hold mid-plan (leaked codenames in reasoning prose, PII in intermediate text). LLM-backed rails here multiply classifier calls per turn — prefer deterministic rails.
  4. tool_args — a tool call’s validated arguments, checked before the tool runs. An escalate verdict converts into a human approval gate.
  5. tool_result — a tool’s output, checked before it enters memory (and before tool result offloading, so rails always see the full text).

From one string to full control

One string

"default" is deterministic and free — no classifier calls: DetectPII(action="redact") + RedactSecrets() + PromptInjection(action="block").

Shorthands

Each shorthand is name or name:action. Valid names: pii, secrets, injection, keywords, moderation, length, topic, judge. A typo raises immediately with the valid options.

Configured built-ins

Per-stage actions live on the rail: on_input=, on_output=, on_tool_args=, on_tool_result= override the rail’s default action (and implicitly opt the rail into that stage).

Plain callables

Return-value coercion: True/None allow, False blocks, a str replaces the content, a Verdict gives full control. Sync or async; take (text) or (text, ctx). @guardrail(stages=[...]) works as a decorator.

LLM judge in one line

With action="retry" the judge’s critique is fed back to the model and the response is re-generated — bounded by Agent(max_guardrail_retries=2). Exhaustion blocks with the last reason.

Rubric quality gates

For a structured definition of “done”, give the judge a rubric instead of one criteria string. Each criterion is graded by its own isolated judge call (pass / fail / unknown + reason), and the failing criteria — with the judges’ reasons — become the revision feedback:
Per-criterion results land in the GuardrailEvent metadata and the run report (metadata["guardrails"]["triggered"][i]["metadata"]["rubric"]). The same rubric works in evals via the rubric! validator — write it once, gate at runtime and regress in CI. Write criteria around verifiable structure, not facts the judge cannot check.

Verdicts

Every check resolves to one of six actions:

Blocked responses

A block is a controlled stop, not an exception:
blocked_message (per rail) is the user-safe copy; reason is the dev-facing explanation that lands in events and traces. The blocked reply is also appended to memory, so the next turn resumes a coherent conversation.

Escalating to a human

A tool_args rail can require human sign-off instead of deciding itself, reusing the whole approval-gate machineryApprovalEvent, resume, edit-on-approve, audit trail:
Tool-local rails also work directly on a Tool (with or without an agent):

Shadow mode

Deploy rails with zero enforcement risk: everything runs and gets recorded — nothing acts.
Verdicts appear in GuardrailEvents (with shadow=True), the run report, and usage counters (guardrails:shadow_triggered), so you can watch trigger rates in traces before flipping to "enforce" (the default). Per-rail: DetectPII(shadow=True).

Sampled online monitoring

Combine shadow mode with sample_rate to grade a slice of production traffic — the online-evaluation pattern, without the request-path cost:
Sampled-out checks record nothing. sample_rate exists for shadow/warn monitoring: sampling an enforcing rail creates nondeterministic enforcement gaps (and buffer-until-verdict still engages on every run, because the streaming decision precedes the sampling roll) — configuring that logs a warning.

Streaming

  • Rails that only redact (deterministic detectors) transform text and thinking deltas in flight, with a per-content-block holdback window so patterns spanning chunk boundaries are still caught.
  • Any rail that can block / retry / escalate forces buffer-until-verdict: deltas are withheld and replayed once the rails allow the message — a blocked response never leaks a single chunk. This trades streaming latency for enforcement; use warn/shadow rails if you need live streaming with observation only.
  • Thinking blocks on stored messages are scrubbed by the redact rails too, so reasoning never carries PII into memory.
  • GuardrailEvent is a first-class stream event, so UIs can show “response withheld” the moment a rail fires.

Observability

Every triggered rail (including shadowed and crashed ones) produces:
  • a GuardrailEvent in the stream: {rail, stage, action, reason, latency_ms, shadow};
  • an entry in the per-run report on OutputEvent.metadata["guardrails"]["triggered"];
  • usage counters: guardrails:triggered / guardrails:shadow_triggered (judge/classifier token usage folds into normal usage accounting).
Introspect a configuration with agent.explain_guardrails() — a table of rails, stages, actions, and order. If a rail itself crashes, the default is fail-open (the run continues; the crash is recorded with action="error"). Set strict=True on security-critical rails to fail closed.

Redacting traces

In-run guardrails redact agent memory and outputs, but traces record every span — including the inner LLM call. trace_redactor closes that gap at the storage/export boundary:
The redactor runs inside the provider’s put() on copies of every span — inputs, outputs, memory dumps, errors, metadata — before storage and before every exporter fires. The live run is never mutated. It works with any provider (JSONL, SQLite, platform, custom) and accepts the same specs as guardrails=, restricted to deterministic rails (trace_redactor("pii:redact", DetectPII(types=["ssn"], redaction="hash"))) — an LLM call per span store would be a footgun, so judgment rails are rejected loudly.
Resumed sessions load memory from stored traces, so chained turns see the redacted history. That is usually exactly what you want for compliance — the raw text exists only inside the run that produced it.

Testing rails without an agent

check_guardrails runs only the rails — no LLM loop, no tools — against any agent or spec, at any stage (stage="model_output", …).

Built-in rails

Deterministic rails cost nothing and add microseconds. LLM-backed rails are explicit opt-ins — point them at a small model.

What the injection pattern pack does and does not catch

The pack is English and literal by design: it is a cheap first filter, not a classifier. It is regression-tested against a corpus of known attacks and benign lookalikes, and it deliberately does not fire on ordinary phrasing that shares its vocabulary (“print the instructions for the desk”, “remove the safety guard from my lawnmower”). It does not catch non-English attacks, base64 or character-separated obfuscation, or paraphrases that avoid the keywords entirely. Those need the classifier:
Treat either as defence in depth, not a boundary: the durable mitigations are least-privilege tools, tool_args rails, and approval gates on anything destructive.
Order matters. Every rail is checked against the text as of its position in the list, so a rail placed after a redactor sees the redacted text — put normalizing/redacting rails before judging rails and the judges never receive the raw content. The first non-allow verdict in list order wins. Adjacent rails that cannot rewrite the text are checked concurrently for latency; a redact/retry rail is a barrier. A custom rail that returns replacement text must declare action="redact", otherwise it raises (batching is decided from the action, so an undeclared rewrite would be an invisible ordering bug).

Known limitations

  • model_output rails judge the final assistant message. For intermediate tool-calling text, opt into the model_step stage (every step) or rely on the tool_args / tool_result edges and in-flight scrubbing.
  • retry verdicts regenerate final responses only; a retry fired mid-plan (on a tool-calling step) coerces to block rather than corrupting the tool loop.
  • trace_redactor accepts deterministic rails only, and redacted traces are what resumed sessions load as history.