Security
Input sanitization and prompt injection defense.
All string input values are XML-escaped by default, and a safe tagged template gives you per-value control over escaping, truncation, and wrapping.
Auto-escape (default)
All string input values are XML-escaped before reaching system/prompt functions. No code changes needed:
prompt({
input: z.object({ instruction: z.string() }),
system: ({ input }) => `Do: ${input.instruction}`,
// instruction is auto-escaped — </role> becomes </role>
})Declare rawFields for trusted content that shouldn't be escaped:
prompt({
rawFields: ['indexedHtml'],
input: z.object({ instruction: z.string(), indexedHtml: z.string() }),
// instruction: escaped, indexedHtml: passed through
})Auto-escape only covers string values in the input schema. If you inject untrusted content via rawFields or
raw(), you are responsible for sanitizing it.
Escape table
escapeXml() replaces the five characters that can break out of XML tag attributes or content:
| Character | Replacement |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
safe tag (explicit control)
The safe tagged template auto-escapes every interpolated value via escapeXml(). The template literal strings themselves are not escaped — only the dynamic values are:
import { safe, raw, limit, wrap } from '@crux/core'
safe`
Doc: ${raw(trustedHtml)}
Query: ${limit(userQuery, 500)}
Instruction: ${wrap(instruction)}
`| Helper | Returns | Effect |
|---|---|---|
raw(v) | SafeWrapper | Skip escaping (trusted content) |
limit(v, n) | SafeWrapper | Truncate + escape |
wrap(v) | SafeWrapper | Escape + wrap in <user-input> delimiters |
raw(), limit(), and wrap() return SafeWrapper objects, not strings. They only work inside safe tagged
templates. Using them in regular template literals (`...${wrap(v)}...`) produces [object Object]. For regular
template literals, use userContent() instead.
Helper functions
truncate(text, maxLength?)
Truncates a string to a maximum length. Default maxLength is 10,000. When the text exceeds the limit, the result is sliced and a … [truncated] suffix is appended:
import { truncate } from '@crux/core'
truncate(longArticle) // → first 10,000 chars + "… [truncated]"
truncate(userQuery, 500) // → first 500 chars + "… [truncated]"raw(text)
Returns a SafeWrapper that safe will not escape. Use only inside safe templates, for content you have already sanitized or that comes from a trusted source:
safe`
System-generated HTML: ${raw(trustedHtml)}
User query: ${userQuery}
`
// trustedHtml is interpolated as-is; userQuery is escapedraw() bypasses all escaping. Only use it when you are certain the content is safe.limit(text, maxLength?)
Combines truncate and escapeXml in a single call. Returns a SafeWrapper — use only inside safe templates:
safe`Query: ${limit(userQuery, 500)}`
// userQuery is truncated to 500 chars, then XML-escapedwrap(text, tag?)
Escapes the text and wraps it in <user-input> delimiters. Returns a SafeWrapper — use only inside safe templates. Delimiters make it clear to the model where user content starts and ends, reducing the chance of instruction following from injected content:
safe`Instruction: ${wrap(instruction)}`
// → Instruction: <user-input>escaped content</user-input>
safe`${wrap(feedback, 'customer-feedback')}`
// → <customer-feedback>escaped content</customer-feedback>Never use wrap() in regular template literals. It returns an object, not a string — you'll get [object Object]
in your prompt. Use userContent() instead for regular templates.
userContent(text, tag?)
Standalone version of wrap() that returns a plain string. Escapes the text and wraps it in <user-input> delimiters. Use this in regular template literals or anywhere outside safe templates:
const prompt = `Instruction: ${userContent(instruction)}`
// → Instruction: <user-input>escaped content</user-input>
const prompt = `Feedback: ${userContent(feedback, 'customer-feedback')}`
// → Feedback: <customer-feedback>escaped content</customer-feedback>Dev warnings
securityWarnings
Security warnings are enabled by default in development (NODE_ENV !== 'production') and disabled in production. When enabled, Crux runs detectSuspiciousPatterns on all input values and logs warnings to the console. Override explicitly if needed:
config({ generation: { securityWarnings: false } }) // disable in dev
config({ generation: { securityWarnings: true } }) // enable in proddetectSuspiciousPatterns(text)
A dev-time heuristic that returns an array of warning strings. It checks for three categories of suspicious input:
| Category | Examples detected |
|---|---|
| Role injection | </system>, </assistant>, <|im_start|> — attempts to close or open model-level message delimiters |
| Instruction override | ignore previous, disregard, new instructions — attempts to override system-level instructions |
| Delimiter manipulation | ---, ===, *** — attempts to inject visual separators that could confuse prompt structure |
import { detectSuspiciousPatterns } from '@crux/core'
const warnings = detectSuspiciousPatterns(userInput)
if (warnings.length > 0) {
console.warn('Suspicious input:', warnings)
}detectSuspiciousPatterns is a heuristic, not a guarantee. It catches common patterns but is not foolproof — treat it
as an extra layer of defense, not the only one.
Audit trail
Combine middleware with detectSuspiciousPatterns() to log potential injection attempts to your monitoring system:
import { detectSuspiciousPatterns } from '@crux/core'
config({
generation: {
middleware: async (args, next) => {
const input = args.preparedArgs?.input
if (input && typeof input === 'object') {
for (const [key, value] of Object.entries(input)) {
if (typeof value === 'string') {
const patterns = detectSuspiciousPatterns(value)
if (patterns.length > 0) {
myLogger.send({
type: 'security.warning',
promptId: args.promptId,
field: key,
patterns,
timestamp: new Date().toISOString(),
})
}
}
}
}
return next(args)
},
},
})This logs every suspicious input without blocking the request. Review the logs to identify attack patterns and harden your prompts.
Security events in devtools
When devtools is enabled and securityWarnings is active, suspicious input patterns are automatically emitted as security:warning events to the devtools dashboard. No custom middleware needed.
What you get
- Timeline badges: Traces with security warnings show a shield badge with count
- Security filter: Filter the timeline to show only traces with warnings
- Dashboard stats: Total warning count in the stats overview
- Dedicated Security tab: Attack timeline, per-prompt vulnerability matrix, and highlighted input previews
- CLI TUI: Warning count in the
crux dev --tuistats panel
How it works
Security warnings are emitted from the devtools middleware with full trace correlation — each warning links to the generate() call that triggered it. The onSecurityWarning instrumentation hook is also available for custom integrations:
import { updateRuntime } from '@crux/core'
updateRuntime({
instrumentationHooks: {
onSecurityWarning: (event) => {
// event: { promptId, field, pattern, message, inputPreview }
myLogger.send(event)
},
},
})Next steps
Core Concepts
How contexts, prompts, and adapters compose together.
Type System
Input merging, conditional return types, and settings.
Middleware & Hooks
Global middleware for logging and error handling.
API Reference
Full security utility signatures — safe, escapeXml, limit, wrap, detectSuspiciousPatterns.