How OmniRoute's Guardrails System Protects AI Requests: PII Masking and Injection Defense

OmniRoute's guardrails system runs configurable pre-call and post-call hooks to mask PII and detect prompt injection attacks before they reach downstream providers.

The guardrails framework in the diegosouzapw/OmniRoute repository provides a modular security layer that intercepts every inbound chat request. It combines PII redaction with prompt injection detection to sanitize payloads and block malicious inputs via a registry-based pipeline.

How the OmniRoute Guardrails Pipeline Works

The system processes requests through a strict sequence defined in src/lib/guardrails/registry.ts. Every HTTP request traverses the following path:

  1. Resolution –_disabled guardrails are identified from headers, metadata, or API key settings.
  2. Pre-call execution – registered guardrails execute preCall methods to inspect and modify the payload.
  3. Downstream execution – the sanitized request reaches LLM providers (OpenAI, Anthropic, etc.).
  4. Post-call execution – guardrails execute postCall methods to redact sensitive data from responses.

Guardrails execute in priority order: PII Masker (10) runs before Prompt Injection (20), ensuring personal data is stripped before injection analysis occurs.

Resolving Disabled Guardrails

Before running the pipeline, resolveDisabledGuardrails() aggregates configuration from three sources:

  • HTTP headers: x-omniroute-disabled-guardrails (or legacy x-disabled-guardrails)
  • Request body: metadata.disabledGuardrails array
  • API key metadata: database-stored disabledGuardrails column
// src/lib/guardrails/registry.ts
export function resolveDisabledGuardrails({
  apiKeyInfo,
  body,
  headers,
}: {
  apiKeyInfo?: Record<string, unknown> | null;
  body?: unknown;
  headers?: HeadersLike;
}): string[] {
  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);
}

The resulting array (e.g., ["pii-masker", "prompt-injection"]) is passed to the registry via the GuardrailContext.

Pre-Call Guardrail Execution

The core chat handler in src/sse/handlers/chat.ts invokes the registry at approximately line 415:

const preCallGuardrails = await guardrailRegistry.runPreCallHooks(body, {
  log: requestLogger,
  disabledGuardrails,
});

if (preCallGuardrails.blocked) {
  return NextResponse.json({ error: preCallGuardrails.message }, { status: 400 });
}

body = preCallGuardrails.payload; // mutated by guardrails

runPreCallHooks iterates over registered guardrails, skips disabled ones, and executes their preCall methods. Each execution can:

  • Block the request (block: true) and return an error message
  • Modify the payload (modifiedPayload) for sanitization
  • Pass silently with metadata logging only

The registry automatically logs each result:

logger.debug?.(
  "GUARDRAIL",
  `${guardrail.name} pre-call ${execution.blocked ? "blocked" : modified ? "modified" : "passed"}`
);

Prompt Injection Protection

The PromptInjectionGuardrail class in src/lib/guardrails/promptInjection.ts implements multi-layered attack detection.

Configuration and Modes

Behavior is controlled via environment variables and runtime options:

Source Variable Effect
Environment INPUT_SANITIZER_MODE block, warn, or log (default: warn)
Environment INPUT_SANITIZER_ENABLED Set to "false" to disable entirely
Feature Flag INJECTION_GUARD_MODE Database-backed override taking precedence
Runtime blockThreshold Severity threshold: low, medium, or high
// src/lib/guardrails/promptInjection.ts
export interface PromptInjectionGuardrailOptions {
  blockThreshold?: "low" | "medium" | "high";
  customPatterns?: PatternLike[];
  enabled?: boolean;
  mode?: "block" | "warn" | "log";
  priority?: number;
}

Detection Engine and Pattern Matching

The evaluatePromptInjection function performs a bounded scan of the first 16 KB (MAX_INJECTION_SCAN_BYTES) for performance:

  1. Calls sanitizeRequest for built-in injection and PII detection
  2. Flattens message contents via extractMessageContents
  3. Runs regex patterns against detectWithPatterns
  4. Combines and deduplicates results
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 and Thresholds

The shouldBlock function evaluates severity scores:

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);
}

When mode is "block" and the threshold is exceeded, the guardrail returns:

{
  block: true,
  message: "Request rejected: suspicious content detected",
  meta: { detections: decision.result.detections.length }
}

Otherwise, it passes with metadata indicating flagged content.

PII Masking in the Request Pipeline

The PIIMaskerGuardrail in src/lib/guardrails/piiMasker.ts operates during both pre-call and post-call phases, activated when PII_REDACTION_ENABLED equals "true" and INPUT_SANITIZER_MODE equals "redact".

Pre-Call Redaction

The guardrail deep-clones payloads and traverses string fields to redact detected personal information:

// src/lib/guardrails/piiMasker.ts
function cloneAndMaskRequestPayload(payload: unknown) {
  const clonedPayload: JsonRecord = JSON.parse(JSON.stringify(payload));
  
  if (typeof clonedPayload.system === "string") {
    // redact system prompt
  }
  
  if (Array.isArray(clonedPayload.messages)) {
    // redact user and assistant messages
  }
  
  return { detections, modified, payload: modified ? clonedPayload : payload };
}

Post-Call Redaction

After the provider responds, the postCall method sanitizes outputs via sanitizePIIResponse and maskResponsesOutput:

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 
  };
}

If no PII is detected, the guardrail returns { block: false } without modification.

Integration in the Core Chat Handler

The complete flow in src/sse/handlers/chat.ts demonstrates the guardrails system integration:

import { guardrailRegistry, resolveDisabledGuardrails } from "@/lib/guardrails";

export async function handleChat(request) {
  const { body, headers } = request;
  const disabledGuardrails = resolveDisabledGuardrails({ apiKeyInfo, body, headers });

  // Pre-call guardrails (PII stripping + injection detection)
  const pre = await guardrailRegistry.runPreCallHooks(body, { 
    log: logger, 
    disabledGuardrails 
  });
  
  if (pre.blocked) return errorResponse(pre.message);
  
  // Execute against LLM provider
  const providerResponse = await executeProvider(pre.payload);
  
  // Post-call guardrails (response PII redaction)
  const post = await guardrailRegistry.runPostCallHooks(providerResponse, { 
    log: logger, 
    disabledGuardrails 
  });
  
  return formatResponse(post.response);
}

This architecture ensures that no raw request reaches the provider without inspection, and no response reaches the client without sanitization.

Configuration and Customization

Disabling Guardrails via Headers and Metadata

To bypass specific guardrails for a single request:

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

Alternatively, via request metadata:

{
  "metadata": { 
    "disabledGuardrails": ["prompt-injection"] 
  },
  "messages": []
}

Environment Variables and Feature Flags

Enable aggressive blocking and PII redaction globally:


# Block on high-severity injections

export INPUT_SANITIZER_MODE=block

# Enable PII redaction

export PII_REDACTION_ENABLED=true
export INPUT_SANITIZER_MODE=redact

# Optional: Override via database feature flag

export INJECTION_GUARD_MODE=block

For custom implementations, register guardrails with specific priorities:

guardrailRegistry.register(
  new PromptInjectionGuardrail({
    blockThreshold: "medium",
    customPatterns: [/secret\s+token/i, "DROP TABLE"],
    mode: "block",
    priority: 5,
  })
);

Summary

  • Modular execution: The GuardrailRegistry in src/lib/guardrails/registry.ts orchestrates pre-call and post-call hooks with configurable priority levels.
  • PII protection: Deep-copy redaction runs before injection detection (priority 10 vs 20) to prevent personal data from interfering with security patterns.
  • Injection detection: Bounded 16 KB scans with customizable regex patterns and severity thresholds (low/medium/high).
  • Flexible disabling: Header, body metadata, or API key settings can disable guardrails per-request without code changes.
  • Fail-closed option: Set INPUT_SANITIZER_MODE=block to reject suspicious requests rather than merely logging them.

Frequently Asked Questions

How do I disable the guardrails system for specific API requests?

Send the x-omniroute-disabled-guardrails HTTP header with a comma-separated list of guardrail names (e.g., pii-masker,prompt-injection), or include "disabledGuardrails": ["prompt-injection"] in the request metadata body. The system also respects legacy header names for backward compatibility.

What happens when the prompt injection guardrail detects suspicious content?

Behavior depends on the configured mode. In block mode (set via INPUT_SANITIZER_MODE or INJECTION_GUARD_MODE), the request returns a 400 error with the message "Request rejected: suspicious content detected" before reaching the LLM provider. In warn mode (default), the request proceeds but logs detection metadata. In log mode, detections are recorded silently without warnings.

Why does PII masking run before prompt injection detection?

The guardrails execute in priority order: PII Masker uses priority 10 while Prompt Injection uses priority 20. This ensures that sensitive personal information is redacted from the payload before the injection scanner analyzes the text, preventing false positives where PII patterns might resemble injection syntax and ensuring user privacy during security analysis.

Can I add custom injection patterns beyond the built-in detection rules?

Yes. When registering the PromptInjectionGuardrail class, pass a customPatterns array containing regex patterns or string literals. These patterns are evaluated alongside default rules in detectWithPatterns(), and you can assign custom severity levels to each pattern for threshold-based blocking.

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 →