How OmniRoute Implements Guardrails: PII Masking and Prompt Injection Detection Explained

OmniRoute's guardrails framework protects every incoming request by running pre-call and post-call hooks that mask personally identifiable information (PII) and detect prompt injection attacks before they reach downstream LLM providers.

The guardrails system in OmniRoute is a TypeScript-based security layer that sits between HTTP clients and AI model providers like OpenAI and Anthropic. It operates through a centralized registry pattern, executing configurable pipelines that can modify, block, or log requests based on security policies. This article breaks down exactly how PII masking and prompt injection detection work in the OmniRoute codebase, with direct references to the implementation files and configuration options.

Guardrails Architecture: Pre-Call and Post-Call Hooks

OmniRoute processes every chat completion request through a two-stage guardrail pipeline orchestrated by GuardrailRegistry. The registry loads guardrail implementations, resolves which are disabled, and executes them in priority order.

The execution flow in src/sse/handlers/chat.ts (~line 415) shows the integration:

// src/sse/handlers/chat.ts
const preCallGuardrails = await guardrailRegistry.runPreCallHooks(body, {
  log: requestLogger,
  disabledGuardrails,
});
if (preCallGuardrails.blocked) {
  return NextResponse.json({ error: preCallGuardrails.message }, { status: 400 });
}
body = preCallGuardrails.payload;   // may be redacted

// ... execute provider ...

const postCallGuardrails = await guardrailRegistry.runPostCallHooks(providerResponse, {
  log: requestLogger,
  disabledGuardrails,
});

Guardrails implement both preCall and postCall methods. PII masking runs in both stages (sanitizing request input and response output), while prompt injection detection runs only pre-call to block malicious prompts before they reach the model.

Guardrail Priority and Execution Order

Guardrails execute by priority number—lower values run first:

Guardrail Priority Stage(s)
PIIMaskerGuardrail 10 pre-call + post-call
PromptInjectionGuardrail 20 pre-call
VisionBridgeGuardrail 30+ pre-call

This ordering ensures PII is stripped before injection analysis, preventing personal data from interfering with pattern detection.

Resolving Disabled Guardrails

Before any guardrail runs, resolveDisabledGuardrails in src/lib/guardrails/registry.ts (lines 52-78) collects disable instructions from three sources:

// src/lib/guardrails/registry.ts
export function resolveDisabledGuardrails({
  apiKeyInfo,
  body,
  headers,
}: {
  apiKeyInfo?: Record<string, unknown> | null;
  body?: unknown;
  headers?: HeadersLike;
}): string[] {
  const apiKeyDisabled = apiKeyInfo?.disabledGuardrails;
  
  const bodyRecord = body as Record<string, unknown>;
  const metadata = bodyRecord?.metadata as Record<string, unknown>;

  const headerDisabled =
    getHeaderValue(headers, "x-omniroute-disabled-guardrails") ||
    getHeaderValue(headers, "x-disabled-guardrails");

  return [...coerceDisabledGuardrails(apiKeyDisabled)]
    .concat(coerceDisabledGuardrails(bodyRecord?.disabledGuardrails))
    .concat(coerceDisabledGuardrails(metadata?.disabledGuardrails))
    .concat(coerceDisabledGuardrails(headerDisabled))
    .filter((value, index, list) => list.indexOf(value) === index);
}

Disable sources are merged and deduplicated. A typical result like ["pii-masker", "prompt-injection"] prevents both guardrails from executing for that request.

Prompt Injection Detection Implementation

The PromptInjectionGuardrail in src/lib/guardrails/promptInjection.ts provides configurable scanning of request content for attack patterns.

Configuration Hierarchy

The guardrail reads settings from multiple layers, with later sources overriding earlier ones:

  1. Environment variables: INPUT_SANITIZER_MODE (block | warn | log, default "warn"), INPUT_SANITIZER_ENABLED
  2. Feature flags: INJECTION_GUARD_MODE from database (takes precedence over env)
  3. Runtime options: passed to constructor

The options interface (lines 34-51) defines the full configuration surface:

// src/lib/guardrails/promptInjection.ts
export interface PromptInjectionGuardrailOptions {
  blockThreshold?: "low" | "medium" | "high";
  customPatterns?: PatternLike[];
  enabled?: boolean;
  logger?: GuardrailContext["log"];
  mode?: "block" | "warn" | "log";
  priority?: number;
}

Detection Algorithm: evaluatePromptInjection

The core detection logic performs five steps:

  1. Built-in sanitization – calls sanitizeRequest from shared/utils/inputSanitizer
  2. Content extraction – flattens all message strings via extractMessageContents
  3. Bounded scan – limits analysis to first MAX_INJECTION_SCAN_BYTES (~16 KB) for performance
  4. Pattern matching – runs detectWithPatterns against default and custom regex patterns
  5. Severity evaluation – combines results and determines block vs. flag vs. pass

The pattern matching implementation (lines 89-104):

// src/lib/guardrails/promptInjection.ts
function detectWithPatterns(
  text: string,
  patterns: ReturnType<typeof normalizePatternEntry>[]
) {
  const detections: Detection[] = [];
  for (const rule of patterns) {
    const match = text.match(rule.pattern);
    if (match) {
      detections.push({
        pattern: rule.name,
        severity: rule.severity,
        match: match[0].slice(0, 50)
      });
    }
  }
  return detections;
}

Blocking logic uses severity scoring (lines 118-127):

// src/lib/guardrails/promptInjection.ts
function shouldBlock(
  detections: Detection[],
  threshold: "low" | "medium" | "high"
) {
  const minimumSeverity = SEVERITY_SCORES[threshold] || SEVERITY_SCORES.high;
  return detections.some(
    d => (SEVERITY_SCORES[d.severity] || 0) >= minimumSeverity
  );
}

Pre-Call Result Handling

The preCall method (lines 60-82) returns structured decisions:

// src/lib/guardrails/promptInjection.ts
async preCall(
  payload: unknown,
  context: GuardrailContext
): Promise<GuardrailResult<unknown>> {
  const decision = evaluatePromptInjection(payload, this.options, context);
  
  if (decision.blocked) {
    return {
      block: true,
      message: "Request rejected: suspicious content detected",
      meta: {
        detections: decision.result.detections.length,
        piiDetections: decision.result.piiDetections.length
      },
    };
  }
  
  return {
    block: false,
    meta: decision.result.flagged
      ? { detections: decision.result.detections.length, piiDetections: decision.result.piiDetections.length }
      : null,
  };
}

When block: true, the chat handler aborts immediately with HTTP 400. In warn or log modes, execution continues with metadata attached for observability.

PII Masking Implementation

The PIIMaskerGuardrail in src/lib/guardrails/piiMasker.ts provides deep, recursive redaction of personally identifiable information in both directions of the request-response cycle.

Enablement Conditions

PII masking activates only when both environment variables are set:

PII_REDACTION_ENABLED=true
INPUT_SANITIZER_MODE=redact

This dual-gate design prevents accidental enabling and allows independent control of sanitization behavior.

Pre-Call Redaction: cloneAndMaskRequestPayload

The guardrail creates a deep copy via JSON.parse(JSON.stringify(payload)) to avoid mutating the original request. It then traverses known fields (system, messages, input, etc.) applying processPII to each string:

// src/lib/guardrails/piiMasker.ts – lines 73-85
function cloneAndMaskRequestPayload(payload: unknown) {
  const clonedPayload: JsonRecord = JSON.parse(JSON.stringify(payload));
  // field-specific redaction logic...
  return { detections, modified, payload: modified ? clonedPayload : payload };
}

The method returns { modified: true, payload: redactedCopy } when changes occur, or { modified: false, payload: original } for passthrough efficiency.

Post-Call Redaction: sanitizePIIResponse

After the provider responds, the post-call hook (lines 88-107) applies the same PII regexes to the response structure:

// src/lib/guardrails/piiMasker.ts
async postCall(
  response: unknown,
  _context: GuardrailContext
): Promise<GuardrailResult<unknown>> {
  const clonedResponse = JSON.parse(JSON.stringify(response)) as JsonRecord;
  const sanitized = sanitizePIIResponse(clonedResponse) as JsonRecord;
  const modifiedResponsesShape = maskResponsesOutput(sanitized);
  
  return {
    block: false,
    meta: { redacted: true },
    modifiedResponse: sanitized
  };
}

The maskResponsesOutput helper additionally walks any output arrays to redact embedded text content that may appear in non-standard provider response shapes.

Registry Orchestration and Logging

The GuardrailRegistry in src/lib/guardrails/registry.ts provides the execution framework for all guardrails. Key responsibilities:

  • Registration: Guardrails self-register via register(guardrail: Guardrail) with priority ordering
  • Pre-call execution: runPreCallHooks iterates guardrails, skips disabled ones, aggregates modifications, and short-circuits on first block
  • Post-call execution: runPostCallHooks applies response transformations in priority order

The registry emits structured debug logs for each guardrail execution (lines 49-55):

// src/lib/guardrails/registry.ts
logger.debug?.(
  "GUARDRAIL",
  `${guardrail.name} pre-call ${execution.blocked ? "blocked" : modified ? "modified" : "passed"}`,
  meta || undefined
);

This logging enables audit trails and debugging of guardrail behavior in production.

Configuration Examples

Disabling Guardrails Per-Request

HTTP header (preferred):

POST /v1/chat/completions
x-omniroute-disabled-guardrails: pii-masker,prompt-injection

{"messages": [{"role": "user", "content": "Hello"}]}

Request body metadata:

{
  "metadata": { "disabledGuardrails": ["pii-masker"] },
  "messages": [{"role": "user", "content": "My SSN is 123-45-6789"}]
}

Enabling Aggressive Injection Blocking


# Environment-level configuration

export INPUT_SANITIZER_MODE=block
export INJECTION_GUARD_MODE=block  # feature-flag override from database

Custom Pattern Registration (Code)

import { PromptInjectionGuardrail } from "@/lib/guardrails/promptInjection";

guardrailRegistry.register(
  new PromptInjectionGuardrail({
    blockThreshold: "medium",
    customPatterns: [
      /bypass\s+security/i,
      /ignore\s+previous\s+instructions/i,
      { pattern: /admin\s+password/i, severity: "high", name: "credential-leak" }
    ],
    mode: "block",
    priority: 5,  // run before standard guardrails
  })
);

Summary

  • GuardrailRegistry orchestrates all security checks through configurable pre-call and post-call hooks, with priority-based ordering and per-request disablement via headers, body metadata, or API key configuration.

  • PromptInjectionGuardrail performs bounded 16 KB scans of request content using built-in patterns and custom regexes, with three operational modes (block, warn, log) and configurable severity thresholds (low, medium, high).

  • PIIMaskerGuardrail creates deep-copied, redacted versions of payloads when PII_REDACTION_ENABLED=true and INPUT_SANITIZER_MODE=redact, applying to both incoming requests and outgoing responses without mutating originals.

  • Configuration layering allows environment variables, database feature flags, and runtime options to control guardrail behavior, with explicit disable mechanisms for testing and emergency bypass.

Frequently Asked Questions

How does OmniRoute prioritize which guardrail runs first?

OmniRoute uses numeric priority values where lower numbers execute first. The default configuration assigns PIIMaskerGuardrail priority 10, PromptInjectionGuardrail priority 20, and VisionBridgeGuardrail priority 30+. This ensures PII is stripped before injection analysis runs, preventing personal data from interfering with attack pattern detection.

Can prompt injection detection block requests entirely, or only log warnings?

The PromptInjectionGuardrail supports three modes controlled by INPUT_SANITIZER_MODE or INJECTION_GUARD_MODE: block rejects the request with HTTP 400 when severity thresholds are met; warn allows execution but attaches detection metadata; and log records findings silently without modifying the response. The default is warn for fail-open safety.

What performance limits does OmniRoute apply to injection scanning?

OmniRoute bounds injection detection to the first MAX_INJECTION_SCAN_BYTES (approximately 16 KB) of extracted message content. This limit prevents denial-of-service through maliciously large payloads while covering typical chat completion requests. The scanning uses standard JavaScript RegExp.match operations against normalized pattern entries.

How do I completely disable PII masking for a specific API key?

Add "pii-masker" to the disabledGuardrails array in the API key's metadata stored in the database, or pass x-omniroute-disabled-guardrails: pii-masker in request headers. Note that PII masking also requires PII_REDACTION_ENABLED=true and INPUT_SANITIZER_MODE=redact to activate—disabling the guardrail alone is sufficient to prevent redaction regardless of environment settings.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →