Architecture

Your AI system prompt is in your bundle

Somewhere in a production bundle right now, there is a paragraph beginning “You are a helpful assistant” followed by several hundred words of carefully tuned instructions, a list of internal tools, and a set of rules describing exactly what the assistant must refuse to do. It was written to be private. It is being served to everyone who loads the page.

What actually ends up in the file

The pattern is consistent across the applications where this goes wrong. A feature starts as a prototype, the prototype calls a model directly from the page because that is the fastest way to see it work, and the prototype ships. What travels to every visitor is rarely just a prompt string. It is usually the whole design of the feature:

  • The system prompt, including the tone rules, the persona and the formatting instructions that took weeks to settle.
  • The tool or function definitions, which describe your internal capabilities by name, with their parameters and descriptions.
  • Model identifiers, temperature and token limits, and any routing logic that picks a cheaper model for some requests and an expensive one for others.
  • Refusal and guardrail instructions, often including a list of the exact phrasings you are trying to catch.
  • Few-shot examples, which are frequently real records because realistic examples work better than invented ones.
  • The provider endpoint, and more often than anyone would like, a key that reaches it.

Read that list as an attacker would. It is a description of what your system can do, what it is told not to do, and how to reach it. The prompt is the headline, but the tool definitions are usually the more valuable disclosure.

Why a protection step does not settle this

The instinct after discovering a prompt in a bundle is to reach for string protection, and it is worth being precise about what that buys. Moving string literals into an encoded table genuinely stops the cheapest attack: a plain text search across loaded sources no longer returns your prompt, and neither does an automated scraper crawling for prompts across the web.

What it does not do is make the text unavailable, and the reason is structural rather than a weakness in any particular implementation. The program has to send the original characters to the model provider. That means the original characters exist, in memory, in the browser, at a moment you can find. Two routes reach them and neither requires reading the transformed code at all:

  • The network panel. If the browser calls the provider, the assembled prompt is in the request body, in plain text, at the moment it is sent.
  • A breakpoint. Pause on the outbound call, or on the decoder, and inspect the value. The transformations changed what the source looks like, not what the values are.

This is the same rule that governs API keys, feature flags and pricing rules in client code, and it is worth stating in its general form: anything the browser must transmit or evaluate is available to whoever controls the browser. Protection changes the cost of finding it, not whether it is there.

The guardrail list is the sharpest disclosure

Of everything in the list above, the part that most reliably surprises teams is the safety instructions. A prompt that ends with a paragraph enumerating the topics the assistant must refuse, the phrasings that indicate an attempted override, and the responses to give when it detects one, is a specification of your detection logic.

Published, it converts evasion from experimentation into a reading exercise. Somebody who wants to get around your restrictions no longer has to discover where the boundaries are, because the boundaries are written down in the file they downloaded. Worse, the list tells them which techniques you did not think of, by omission.

This is not an argument for keeping the list secret and considering the problem solved. Instructions are not an authorisation boundary in either case. It is an argument for putting enforcement where it survives being read: in the server code that executes a tool call, in validation applied to model output before anything acts on it, and in the permissions attached to the authenticated user making the request.

The arrangement that works

The correction is architectural, and it is the same shape as the correction for an API key in client code. The browser stops being a participant in the model conversation and becomes a client of your own service.

The prompt is server-side data

Keep the template where you keep your other server configuration. The browser sends the user input and whatever application context is legitimately the user’s own; your endpoint composes the full prompt. Prompt changes then become a deploy of your service rather than a change to a public artifact, which is also considerably more pleasant to iterate on.

The credential never leaves your infrastructure

Your endpoint authenticates the caller, then calls the provider with a key the page has never seen. This is what makes per-user rate limiting and spend caps possible at all, which matters more than the confidentiality argument the first time somebody points a script at your feature.

Tool execution is authorised on the server

When a model asks for a tool call, the decision to honour it is yours, made against the authenticated user’s permissions, with the arguments validated as untrusted input. A model requesting an action is a suggestion, not an instruction, and treating it as the latter is how a prompt injection turns into a real incident.

Examples containing real data come out

Audit what your few-shot examples actually contain before anything else on this list, because that is the item most likely to be a reportable disclosure rather than a competitive one. Replace real records with synthetic ones that exercise the same shape.

What is genuinely worth protecting in the client

Moving the prompt server-side does not empty the browser. A well-built model feature carries a substantial amount of client-side engineering, and that remainder is a real asset:

  • Streaming response handling, partial rendering and the interruption behaviour that makes a feature feel fast.
  • Context assembly from application state: what the user is looking at, what they selected, what is in scope.
  • Client-side routing heuristics that decide which endpoint or mode a request should take.
  • Caching, deduplication, retry and fallback behaviour.
  • The interface work around all of it, which is usually the visible difference between your product and a competitor’s.

That code has to be in the browser to do its job, and it is exactly the kind of accumulated detail a competitor would rather copy than rebuild. Protecting it raises the effort required to lift it wholesale and to tamper with your shipped bundle undetected. It is the appropriate use of a protection step in an application with model features, and it works precisely because it is aimed at something the browser genuinely has to hold.

A short audit

Open your production bundle and search the loaded sources for the opening words of your system prompt, for the name of any internal tool your model can call, and for the provider hostname. Then open the network panel, use the feature once, and read the outbound request. If the prompt appears in either place, the work described above is worth scheduling. If it does not, check the few-shot examples anyway.

Frequently asked questions

Is a system prompt shipped in front-end code recoverable?

Yes, and by more than one route. If the browser sends the prompt to a model provider, it is visible in the request body in the network panel, whatever the code around it looks like. If the prompt is assembled in the client before sending, a breakpoint on the call recovers the assembled value. String protection changes how hard it is to find the text by searching the file; it does not change the fact that the program must produce the original characters in order to transmit them.

Does encrypting strings hide a prompt from someone reading the bundle?

It raises the cost of the easy attack and leaves the real one intact. Moving text into an encoded table means a plain text search of the file no longer returns your prompt, which genuinely stops casual copying. But every encoded string is decoded at runtime by code that ships in the same file, so anyone willing to set a breakpoint or log the decoder output gets the plain text back. Treat string protection as friction against bulk scraping rather than as confidentiality for a specific secret.

What is the actual risk of publishing our prompt?

Three things, in rising order of seriousness. A competitor copies months of prompt tuning in an afternoon, which is an intellectual property loss rather than a security incident. An attacker reads your guardrail instructions and knows exactly which phrasings you attempted to block, which makes evasion a targeted exercise instead of a guessing game. And if your prompt describes internal tools, data sources or business rules, you have published an architecture document for a system you thought was private.

Should the model call itself happen in the browser?

Only for a prototype, and even then not against a paid account. Calling a provider from page script means the credential is in the page, the prompt is in the request, the model and parameters are visible, and your spending is bounded by whatever the provider enforces rather than by you. The standard arrangement is an endpoint on your own server that authenticates the user, composes the prompt from a template you keep server-side, calls the provider with a credential the browser never sees, and applies your own rate and spend limits.

If the prompt lives on the server, what is left to protect in the client?

More than teams expect. The orchestration around a model feature is real engineering: streaming and partial-response handling, retry and fallback behaviour, client-side classification that decides which endpoint to call, caching, context assembly from application state, and the interface that makes the whole thing feel responsive. None of that can move to a server, all of it is downloadable, and it is frequently the part a competitor would find most useful to copy.

Does knowing the system prompt make prompt injection easier?

It makes crafting an override considerably cheaper, because the attacker can write instructions that speak directly to yours instead of probing blindly. The important corollary is the one people take the wrong way: a secret prompt is not a security control either. Instructions in a prompt are not an authorisation boundary, so the durable protection is enforcing permissions in the code that executes tool calls, validating model output before acting on it, and constraining what any single call is allowed to do.

How should we handle few-shot examples that contain real data?

Remove them from client code entirely, and check what is already in your published bundles. Examples embedded to steer model behaviour routinely contain real customer records, internal identifiers, sample support tickets and account details, because that is what makes them effective. Shipped to a browser, that is a disclosure independent of the prompt question, and it is the version of this problem most likely to become a formal incident rather than a competitive annoyance.

Related reading