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:
- Environment variables:
INPUT_SANITIZER_MODE(block|warn|log, default"warn"),INPUT_SANITIZER_ENABLED - Feature flags:
INJECTION_GUARD_MODEfrom database (takes precedence over env) - 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:
- Built-in sanitization – calls
sanitizeRequestfromshared/utils/inputSanitizer - Content extraction – flattens all message strings via
extractMessageContents - Bounded scan – limits analysis to first
MAX_INJECTION_SCAN_BYTES(~16 KB) for performance - Pattern matching – runs
detectWithPatternsagainst default and custom regex patterns - 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:
runPreCallHooksiterates guardrails, skips disabled ones, aggregates modifications, and short-circuits on first block - Post-call execution:
runPostCallHooksapplies 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=trueandINPUT_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →