Skip to main content
interlace
Plugin: anthropic-securityRules

no-untrusted-content-in-prompt

Disallow untrusted content built into the Anthropic system prompt

Disallow untrusted content built into the Anthropic system prompt.

Why

A system prompt is instruction text. Whatever is spliced into it is read by the model as instructions, not as data โ€” so anyone who controls that value controls the agent's rules. The model has no way to tell "this part of my instructions came from a stranger" once the string is assembled.

This is the same argument as mcp-sdk-security/no-tool-description-injection, one layer down: tool descriptions and system prompts are both model-facing instruction surfaces, and both must be static.

What counts as static

A string literal, a template with no interpolations, or a concatenation of those. A bare identifier also counts โ€” system: SYSTEM_PROMPT is the correct pattern and by far the most common one, and following it is the data-flow analysis this rule deliberately avoids. The interpolation is where the injection is visible in one place, and that is what gets reported.

Not this rule

  • The Vercel AI SDK. generateText({ system }) and streamText({ system }) belong to vercel-ai-security/no-dynamic-system-prompt. This rule gates on Anthropic's own request paths, so no line is ever reported by both.
  • User turns. { role: 'user', content: `${input}` } is the remediation, not the bug โ€” a user message is where runtime values belong, because the model reads them as data.

Known limitations

  • The gate is file-level: once the Anthropic SDK is imported anywhere in the file, any call whose member path matches a request path is inspected. An unrelated object that happens to expose the same path โ€” client.messages.create on something that is not an Anthropic client โ€” is a false positive. Resolving the callee back to the import would need the cross-scope data-flow analysis these rules deliberately avoid; the trade is a rule that stays fast and predictable, and it is why this rule ships in strict only until the corpus run measures the real rate.
  • A system prompt assembled before the call (const p = base + role; then system: p) is a false negative, for the same reason a bare identifier counts as static.

Incorrect

await client.messages.create({ system: `You are an assistant for ${tenantName}.`, messages });

Correct

await client.messages.create({
  system: 'You are a helpful assistant.',
  messages: [{ role: 'user', content: `Tenant: ${tenantName}` }],
});

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.