Guardrails
Boundary-targeted policies that allow, block, warn, or rewrite content before unsafe data crosses a runtime boundary.
Guardrails protect runtime boundaries. They run before content moves into or out of the model, tools, retrieval, memory, validation feedback, or other Safety surfaces. Use a guardrail when you need to block, redact, mask, hash, normalize, or report on content without re-calling the model.
import { boundary, guardrail } from "@use-crux/core/safety";
export const injectionGuard = guardrail({
id: "prompt-injection",
on: boundary.input.text(),
run: guardrail.injection({ action: "block" }),
});
export const piiGuard = guardrail({
id: "pii",
on: [boundary.input.text(), boundary.output.text()],
run: guardrail.pii({ strategy: "mask" }),
});Attach guardrails globally with createSafetyPlugin(), on prompts and contexts,
or per call with guardrails: [...].
Authoring
Every guardrail has a stable id, one boundary or a boundary array, and a
run(subject, ctx) callback. The boundary drives the subject type:
const redactEmail = guardrail({
id: "email-redactor",
on: boundary.output.text(),
run: async (text, ctx) => {
const next = text.replace(/\S+@\S+/g, "[email]");
if (next === text) return { action: "allow" };
return {
action: "rewrite",
value: next,
rewrite: { kind: "redact" },
findings: [{ type: "email", count: 1 }],
};
},
});Supported result actions:
| Action | Effect |
|---|---|
allow | Continue with the current subject. |
block | Stop the call with GuardrailBlockedError. |
warn | Record a Safety decision without changing content. |
rewrite | Replace the subject. Use rewrite.kind for redact, mask, hash, or normalize. |
hold | Streaming only. Wait for more text before releasing the segment. |
strip | Media only. Remove an optional canonical part in enforce mode; report intent without mutation in report mode; block when the part is required. |
Malformed or unknown results fail closed with SafetyResultError, including
when JavaScript callers escape the TypeScript result type.
Built-in Strategies
First-party strategies are provider-agnostic and carry safe strategy metadata for observability, Devtools, and Project Index:
guardrail.pii({ strategy: "redact" });
guardrail.secrets();
guardrail.injection({ action: "warn" });
guardrail.classifier({
classifier: async (subject) => ({ unsafe: subject.includes("risk") }),
blockWhen: (result) => result.unsafe,
});
guardrail.media({
mediaTypes: { allow: ["image/*", "application/pdf"] },
size: { maxBytes: 10 * 1024 * 1024 },
});pii, secrets, and injection are text strategies. They default to
sentence-gated streaming. classifier defaults to final-output checks because
classifier calls are usually too expensive or stateful for every segment.
Boundaries
Common boundaries:
| Boundary | Subject |
|---|---|
boundary.input.text() | User input text before the provider call. |
boundary.input.media() | One canonical input-media part and its stable message/operation origin. |
boundary.output.text() | Model output text before the caller sees it. |
boundary.output.media() | One canonical model/operation output-media part before public accumulation. |
boundary.output.object<T>() | Parsed structured output. |
boundary.output.path<T>()('a.b') | One typed structured-output path. |
boundary.tool.call() | Tool call name, id, and input. |
boundary.tool.result() | Tool result before it returns to the model. |
boundary.memory.write<T>() | Candidate memory write. |
boundary.validation.feedback() | Corrective feedback before it is re-prompted. |
Guardrails can bind to several boundaries with on: [...]. A duplicate
boundary in the same guardrail is a configuration error. Duplicate policy ids
across attached policies are also invalid; use one policy with multiple
boundaries when the same policy should apply in several places.
Multimodal Content
Use guardrail.media() with either media boundary—or an input/output media
tuple—for a declarative content policy. Crux checks the real canonical image,
audio, video, or file part at its exact runtime boundary:
import { boundary, guardrail } from "@use-crux/core/safety";
export const safeAttachments = guardrail({
id: "safe-attachments",
on: boundary.input.media(),
run: guardrail.media({
mediaTypes: {
allow: ["image/png", "image/jpeg", "application/pdf"],
},
size: {
maxBytes: 10 * 1024 * 1024,
},
sources: {
allowHosts: ["cdn.example.com"],
allowInline: true,
allowProviderFiles: true,
allowUrlUserInfo: false,
allowUrlQuery: true,
},
action: "block",
}),
});At least one of mediaTypes, size, or sources is required. MIME patterns
accept exact essences and top-level subtype wildcards such as image/*.
Unknown MIME types and sizes fail their configured rule by default; set that
rule's allowUnknown: true only when the application accepts sources whose
metadata cannot be known locally. A raw remote URL normally has unknown size,
while URL assets and provider-file references can supply a declared size.
Source rules use these categories:
- Bytes, Blobs, data assets, and data URLs are inline.
- Provider-owned file references are provider files, not remote hosts.
- Raw URLs,
URLobjects, and URL assets are remote URLs.
allowHosts performs case-normalized exact hostname matching—no wildcards,
paths, ports, or suffix matching. URL authority userinfo is rejected by
default, while query strings (including signed-URL parameters) are allowed by
default. Set allowUrlQuery: false for a stricter posture. Crux does not use
credential-name heuristics.
Omitting sources performs no source checks. Passing sources: {} opts into
the documented baseline: inline and provider files allowed, URL userinfo
blocked, and query strings allowed. Inspection uses only supplied metadata and
local bytes. It never fetches a URL or asks a provider for metadata. Decisions
can report a MIME type or byte counts, but never include the URL, observed
hostname, path, query, userinfo, filename, provider/file id, or payload bytes.
The violation action defaults to block. Set action: 'strip' to remove only
an offending part in enforce mode. In report mode, both actions record what
would happen while leaving the request unchanged:
const attachmentRollout = guardrail({
id: "attachment-rollout",
mode: "report",
on: boundary.input.media(),
run: guardrail.media({
size: { maxBytes: 10 * 1024 * 1024 },
action: "strip",
}),
});Use a guardrail here because attachments are caller input that must be accepted, blocked, or stripped before the first provider call. Constraints are retryable assertions about model output and cannot repair unsafe input. Input media rewrites remain tracked in #212.
For policy logic beyond the built-in rules, provide a custom media callback.
Narrowing part.type exposes that canonical variant's fields; the callback type
is inferred from the boundary without type arguments.
The callback receives { part, origin }. Narrow origin.kind before reading
message, step, or completed-operation coordinates. Those indexes refer to the
original canonical arrays before any earlier policy stripped a sibling, and
the same coordinates appear in audit and safe observability records. The
original part/source identity is passed through unchanged for inspection.
In the default mode: 'enforce', strip changes the messages sent to the
provider. With mode: 'report', it records a would-strip decision but sends the
original part. If an enforced strip removes the final part of a media-only
message, Crux blocks immediately and attributes the block to that policy and
original location.
Media policies accept allow, warn, block, and strip. They cannot stream,
return text actions such as rewrite or hold, or share an on tuple with a
text boundary.
Output media is guarded once per language step and on bounded-operation
results. Reasoning remains output text and tool-call parts cannot be edited.
For image generation, sibling strips reset result.image to the first retained
entry and a final-image strip blocks. Speech audio is required, so an enforced
strip blocks. Report mode preserves all result fields. Unmodified canonical
parts, raw, provider metadata, warnings, and usage retain their identities;
provider-native surfaces remain outside canonical Safety guarantees.
For stream(), output media is completion-only. A block rejects completion,
but text already emitted through textStream cannot be recalled, and the raw
provider stream remains unguarded. See the complete
content-primitive matrix.
Use boundary.input.text() when a policy instead needs the messageText()
projection. Text parts pass through verbatim and media parts become bounded
descriptors:
- URL sources include the full URL, such as
[image image/png https://example.com/chart.png]. - Byte and data sources include their size and a 12-character SHA-256 prefix,
such as
[image image/png 3B sha256:ab12cd34ef56]. Payloads over 256KB usesha256:omitted. - Blob sources include their size and type with
sha256:unavailable. - File descriptors include the filename, such as
[file application/pdf "report.pdf" ...].
For example, this text guard receives both the text and the image URL:
const mediaSourceGuard = guardrail({
id: "media-source",
on: boundary.input.text(),
run: async (subject) => {
// subject: "Describe this image\n[image image/png https://example.com/chart.png]"
return subject.includes("token=")
? { action: "block", reason: "Media URL contains a token." }
: { action: "allow" };
},
});block and warn apply to the complete text projection. rewrite applies only to
text segments: every media placeholder must remain verbatim so Crux can write
the rewritten text back around the original media parts. A rewrite fails
closed with SafetyResultError if it changes a placeholder, targets a
media-only message, or cannot be redistributed unambiguously. Use
boundary.input.media() to inspect or strip the canonical source instead of
rewriting its text placeholder.
Execution
Adapters create one Safety session per generate() or stream() call. The
session:
- Runs input guardrails over every user message, in order.
- Applies output guardrails before constraints, validation feedback, observability, memory, tools, and final caller access consume generated content.
- Keeps structured
textandobjectsynchronized after rewrites. - Emits safe Safety decisions and audit metadata by default.
Guardrails do not re-call the model. Use a constraint when a semantic failure should retry generation with feedback.
Report Mode And Tuning
Use mode: 'report' when a guardrail should audit but not enforce:
const shadowPii = guardrail({
id: "pii-shadow",
mode: "report",
on: boundary.output.text(),
run: guardrail.pii(),
});You can also tune policy posture per call:
await adapter.generate(prompt, {
input,
safety: {
tune: {
"pii-shadow": { mode: "enforce" },
"prompt-injection": { enabled: false },
},
},
});Tune can change only mode, stream, and enabled. Unknown policy ids throw
so accidental misspellings do not silently weaken policy.
Streaming
Ordinary text guardrails apply to streams. By default, Crux gates text by sentence, runs each guardrail stage, then releases only the cleared segment. Later guardrails see earlier rewrites.
const pii = guardrail({
id: "pii",
on: boundary.output.text(),
stream: "sentence",
run: guardrail.pii({ strategy: "mask" }),
});Streaming options:
| Option | Behavior |
|---|---|
false | Skip the guardrail for streaming calls and audit the skip. |
'final' | Let live chunks flow and run the guardrail once at completion. |
'sentence' | Gate and check complete sentence segments. |
'line' | Gate and check complete line segments. |
'chunk' | Check each provider text chunk. |
{ segment, maxHold, onHoldLimit } | Custom segmenter and hold bounds. |
hold is valid only for non-final stream segments. If held text exceeds the
configured maxHold, Crux fails closed by default with StreamHoldLimitError.
Object and path guardrails run at final output for streams.
Errors And Audit
GuardrailBlockedError is policy-terminal and carries safe decision metadata.
Audit and observability records include policy ids, boundaries, action,
duration, findings, and safe capture summaries. Raw pre-safety content is not
included by default.
import { GuardrailBlockedError } from "@use-crux/core/safety";
try {
await adapter.generate(prompt, { input, guardrails: [injectionGuard] });
} catch (error) {
if (error instanceof GuardrailBlockedError) {
error.decisions[0]?.policyId;
error.decisions[0]?.captured.preview;
}
}Use evaluateGuardrail() for focused unit tests and
ctx.expect.decisionReport.safety.toHaveOutcome(...) in Eval checks when
you want to assert on the canonical Safety decision report.