Crux
GuidesSafety

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.

guardrails.ts
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:

ActionEffect
allowContinue with the current subject.
blockStop the call with GuardrailBlockedError.
warnRecord a Safety decision without changing content.
rewriteReplace the subject. Use rewrite.kind for redact, mask, hash, or normalize.
holdStreaming only. Wait for more text before releasing the segment.
stripMedia 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:

BoundarySubject
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:

safe-attachments.ts
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, URL objects, 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 use sha256: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:

  1. Runs input guardrails over every user message, in order.
  2. Applies output guardrails before constraints, validation feedback, observability, memory, tools, and final caller access consume generated content.
  3. Keeps structured text and object synchronized after rewrites.
  4. 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:

OptionBehavior
falseSkip 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.

On this page