no-dynamic-system-prompt
This rule identifies code patterns where system prompts contain dynamic or user-controlled content
Prevents dynamic content in system prompts to avoid agent confusion attacks.
📊 Rule Details
| Property | Value |
|---|---|
| Type | problem |
| Severity | 🔴 HIGH |
| OWASP Agentic | ASI01: Agent Confusion |
| CWE | CWE-74: Improper Neutralization |
| CVSS | 8.0 |
| Config Default | error (recommended, strict) |
🔍 What This Rule Detects
This rule identifies code patterns where system prompts contain dynamic or user-controlled content. Dynamic system prompts can lead to agent confusion attacks where the AI's core behavior can be manipulated.
Both spellings of the system-prompt option are checked. AI SDK v7 renamed it to
instructions and marks system as deprecated, so the examples below apply
identically to instructions:.
❌ Incorrect Code
// Template literal with expression
await generateText({
system: `You are a ${role} assistant.`,
prompt: userInput,
});
// String concatenation
await generateText({
system: 'You are a ' + role + ' assistant.',
prompt: userInput,
});
// Function call result
await streamText({
system: getSystemPrompt(agentType),
prompt: userInput,
});
// Async system prompt
await generateObject({
system: await fetchSystemPrompt(),
prompt: userInput,
});✅ Correct Code
// Static string literal
await generateText({
system: 'You are a helpful assistant.',
prompt: userInput,
});
// Static constant
const SYSTEM = 'You are a helpful coding assistant.';
await generateText({
system: SYSTEM,
prompt: userInput,
});
// Static template literal (no expressions)
await streamText({
system: `You are a helpful assistant.
You can help with coding tasks.`,
prompt: userInput,
});⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
allowStaticTemplates | boolean | true | Allow template literals without expressions |
🛡️ Why This Matters
Dynamic system prompts enable attackers to:
- Modify AI core behavior - Change fundamental instructions
- Bypass safety measures - Remove content restrictions
- Inject persistent instructions - Add malicious instructions that persist across conversations
- Create agent confusion - Make the AI act inconsistently
🔗 Related Rules
no-system-prompt-leak- Prevent system prompt exposurerequire-validated-prompt- Validate user prompts
🔄 Changes in the 2026-10-10 FP/FN audit
- A template or
+chain built only from string constants (constbindings with literal initialisers) and the current date (new Date(),Date.now()and methods on them) is static and no longer reported. - A system prompt read straight from the request — destructured from
await req.json(),req.body.x,body.xwherebody = await req.json(), orsearchParams.get(...)— is reported with the new messageuserControlledSystemPrompt, even with no template around it. - Options: the documented
defaultOptionsnow apply. Earlier versions readcontext.optionsand silently ran a shorter hard-coded list instead.
🔄 Changes in the 2026-10-11 zero-deferral pass
- The request value is followed through any number of same-file hops:
const body = await req.json(); const persona = body.persona as string;, later reassignments, a same-file helper's return (system = pickPersona(body)), and a same-file function whose parameter is fed from the request. - A call to a same-file function every return of which is static text
(
system: buildSystemPrompt()) is static and no longer reported.
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Static Constant from Dynamic Source
Why: Constant assignment from dynamic source is not traced.
// ❌ NOT DETECTED - Constant assigned dynamically
const SYSTEM = process.env.SYSTEM_PROMPT; // Dynamic!
await generateText({ system: SYSTEM, prompt: userInput });Mitigation: Use hardcoded string literals. Never use env vars for system prompts.
Conditional System Prompt Selection
Why: Conditionally selected prompts may be static but appear dynamic.
// ❌ FALSE POSITIVE RISK - All options are static
const PROMPTS = { admin: 'You are admin.', user: 'You are user.' };
await generateText({ system: PROMPTS[role], prompt: userInput });Mitigation: Use if/else with literal strings instead of object lookup.
Import from Constants File
Why: Imported values are not analyzed.
// ❌ NOT DETECTED - May be dynamic in source file
import { SYSTEM_PROMPT } from './prompts';
await generateText({ system: SYSTEM_PROMPT, prompt: userInput });Mitigation: Apply rule to constants files. Use inline string literals.
📚 References
Use this rule
npm install -D oxlint-plugin-vercel-ai-security{ "jsPlugins": ["oxlint-plugin-vercel-ai-security"], "rules": { "vercel-ai-security/no-dynamic-system-prompt": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-vercel-ai-security'], rules: { 'vercel-ai-security/no-dynamic-system-prompt': 'error', }, },});oxlint-plugin-vercel-ai-security re-exports eslint-plugin-vercel-ai-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.