How do I prevent an AI agent from leaking system prompt content?
To prevent an AI agent from leaking system prompt content, screen the request that asks for it, screen the response that carries it, and keep anything that would hurt you out of the prompt. The third control holds when the other two miss.
A system prompt reaches a user through three paths:
- Direct extraction. Someone asks the model to repeat its instructions, and the model repeats them.
- Indirect extraction. Injected content in a retrieved document or a tool result instructs the model to include its instructions in the answer.
- Incidental disclosure. The model paraphrases its instructions while explaining why it refused something, with no attacker involved.
The third path needs no attacker, and no jailbreak detector fires on it. That's why prompt secrecy can't be the only control.
Why the system prompt isn't a secret
The system prompt shares a context window with attacker-influenced text, and a component whose job is to produce plausible continuations processes it. It has no access control. Treat the system prompt as confidential rather than secret: worth protecting, embarrassing to lose, and not a safe place for anything that matters.
A practical test is to write down what an attacker gains from a verbatim copy of your system prompt. If the answer includes an API key, an internal hostname, a customer's name, a discount code, or the exact bounds of a business rule, then the problem isn't leakage. The problem is that the value is in the prompt.
The following table separates the two cases:
| In the prompt | Risk if disclosed | Fix |
|---|---|---|
| Tone and format instructions | Low. Competitive embarrassment | Screen outputs, accept residual risk |
| Tool descriptions and names | Moderate. Maps the attack surface | Enforce authorization at each tool regardless |
| Business rules and thresholds | High. Reveals what to stay under | Move the rule into code, refer to it abstractly |
| Credentials, keys, internal URLs | Critical. Directly exploitable | Remove. Keep out of prompt text |
| Customer or employee data | Critical. A reportable disclosure | Remove. Fetch per request, with authorization |
Anything in the last two rows is a finding on its own, before you consider extraction. The OWASP Top 10 for LLM Applications covers this pair as LLM02:2026 Sensitive Information Disclosure and LLM08:2026 Hidden Context Exposure. For more information, see implementing the OWASP Top 10 for LLM Applications.
Screen the request that asks for it
Extraction attempts have recognizable shapes: a request to repeat the text that precedes the conversation, to translate or summarize the instructions, to enter a debug or developer mode, or to encode the configuration. A prompt-injection detector catches the well-known phrasings.
The following handler does that with Arcjet, a security library that evaluates its rules in your own process and returns an allow-or-deny decision before the provider call. launchArcjet creates the client, detectPromptInjection() configures the rule once, and arcjet.guard() runs it against the inbound message:
import { detectPromptInjection, launchArcjet } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
export async function handleMessage(message: string, session: Session) { const decision = await arcjet.guard({ label: "chat.inbound", actor: session.userId, rules: [detectPromptInjection()(message)], });
if (decision.conclusion === "DENY") { return { reply: "Please rephrase your message." }; }
return callProvider({ message });}Keep the denial generic. A message that says the request looked like a prompt-extraction attempt tells the attacker which phrasing to vary, and turns your detector into a tuning oracle.
Inbound screening has a known ceiling. A request phrased as a legitimate support question, with no attack pattern, can still lead the model to paraphrase its instructions. Measure the detector in dry-run mode against real traffic before you enforce, so that you know the false-positive rate that you're accepting.
Screen the response that carries it
Most teams skip the output check, and it's the cheaper of the two to get right, because you know what you're looking for. You wrote the system prompt, so you can test whether the response reproduces it.
The following check compares the response against distinctive strings from your own prompt, rather than trying to detect leakage in the abstract:
// Distinctive phrases from the system prompt. Short or common strings// produce false positives, so pick spans that only appear in the prompt.const PROMPT_MARKERS = [ "You are Aria, the billing assistant for", "escalate to a human when the refund exceeds",];
function looksLikePromptDisclosure(reply: string): boolean { const normalized = reply.toLowerCase(); return PROMPT_MARKERS.some((marker) => normalized.includes(marker.toLowerCase()), );}Exact matching catches verbatim reproduction and nothing else. A model that paraphrases gets through. Two additions narrow that gap. Place a unique, meaningless canary string in the system prompt, so that any response containing it is an unambiguous disclosure regardless of the surrounding text. Run a sensitive-info check on the response, so that a paraphrase carrying an internal hostname or an email address is caught by what it contains rather than by how it's worded.
On a match, return a generic reply and record the event with the request's correlation ID. Don't return the model's text with the offending span removed. The remainder often reconstructs the instruction.
The indirect path through retrieved content
A retrieved document can carry an instruction to disclose the system prompt, and that text didn't cross your inbound screen. The user typed a normal question. The payload arrived through a fetched page, a knowledge-base article, a ticket body, or an MCP tool result.
Screen tool results and retrieved chunks before they re-enter the context window, the same way that you screen inbound messages. The following tool runs the same Arcjet prompt-injection rule through the same guard() call, in your own process, this time on fetched content, and returns a placeholder on a deny:
import { detectPromptInjection, launchArcjet } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
export async function fetchTool({ url }: { url: string }) { const content = await fetch(url).then((r) => r.text()); const decision = await arcjet.guard({ label: "tools.fetch", rules: [detectPromptInjection()(content)], });
if (decision.conclusion === "DENY") { return { content: "[Content blocked]" }; }
return { content };}For more information about the full shape of that attack, see indirect prompt injection in agentic workflows and how to secure a RAG application.
Move the value out of the prompt
Prompt secrecy is a weak control, so design for it to fail. Three moves reduce what a disclosure exposes to almost nothing.
Keep secrets out. Credentials, keys, and internal hostnames belong in environment variables that your tool handlers read. The model doesn't need them to call a function that you wrote. For more information, see storing secrets in environment variables.
Move the rules into code. A prompt line such as Refunds above $500 need approval is both leakable and unenforceable. The same rule as a threshold in issueRefund is neither. For more information about that placement, see how to limit what actions an AI agent is allowed to take.
Fetch data per request, with authorization. Customer context assembled into the prompt at request time, scoped by the session, leaks one user's data at worst. The same data preloaded into a shared system prompt leaks everyone's. For more information, see preventing LLMs from surfacing confidential data.
After those three moves, a full disclosure of the system prompt reveals your tone instructions and the names of your tools. The tool names matter only if the tools themselves aren't authorized, which brings the problem back to the layer that was going to decide it anyway.
Where Arcjet fits
Arcjet covers two of the three controls on this page. Prompt-injection detection screens the inbound message and the content that re-enters context. Sensitive-information detection classifies the response in your own process, so a paraphrase that carries an internal hostname or an email address is caught without the text leaving your environment. Both run from one guard() call and return a decision before the reply is sent.
The third control, moving credentials and thresholds out of the prompt, is a design decision that no library makes for you.
Learn more: Prompt injection detection ยท Sensitive information detection
System prompt leakage checklist
- No credentials, keys, internal hostnames, or customer data appear in prompt text.
- Business thresholds live in code, and the prompt refers to them without stating values.
- A prompt-injection check runs on inbound messages before the provider call.
- The same check runs on tool results and retrieved chunks before they re-enter context.
- A canary string in the system prompt makes verbatim disclosure unambiguous.
- Responses are checked against distinctive prompt markers and for sensitive information.
- Denials are generic and don't name what the detector matched.
- Suspected disclosures are recorded with the actor and correlation ID for review.
Frequently asked questions
How do I prevent an AI agent from leaking system prompt content?
Run a prompt-injection check on inbound messages, check responses against distinctive markers from your own prompt, and remove anything from the prompt that would actually hurt you if disclosed. The third control holds when the first two miss.
Is the system prompt a secret?
No. It shares a context window with attacker-influenced text and is processed by a component that produces plausible continuations, with no access control. Treat it as confidential rather than secret, and keep credentials, internal hostnames, and customer data out of it.
How do I detect that a response contains the system prompt?
Compare the response against distinctive spans from your own prompt rather than detecting leakage in the abstract. Add a unique canary string to the system prompt so verbatim disclosure is unambiguous, and run a sensitive-info check so a paraphrase carrying an internal hostname is caught by content.
Can retrieved content cause a system prompt disclosure?
Yes. A fetched page, knowledge-base article, or MCP tool result can carry an instruction to disclose the prompt, and that text never crossed your inbound screen. Screen tool results and retrieved chunks before they re-enter the context window.
What should a denial message say?
As little as possible. A message naming what the detector matched tells the attacker which phrasing to vary and turns the detector into a tuning oracle. Return a generic reply and record the event with the correlation ID.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Score the text inside your own request lifecycle, before it reaches the model, and treat the score as a signal rather than an authorization.