Output Boundary Encoding
Treat everything the model emits as hostile input to whatever consumes it. Encode and validate at each destination — renderer, shell, query, downstream agent — using that destination's own rules. One global sanitiser cannot do this: escaping that is correct for HTML is meaningless for a shell.
Definition
Output boundary encoding is the practice of applying destination-specific encoding and validation to model output at every point where it crosses into a system that will interpret it, rather than filtering it once, generically, on the way out of the model. Its absence is the weakness OWASP names as improper output handling.
Problem
Teams harden the input side against prompt injection and leave the output side open. The model's text then reaches a renderer, a shell, a database or another agent's context, where it is interpreted as instruction rather than displayed as data. The attacker never has to reach the model directly.
When to use it
Any agent whose output reaches something that parses it: a chat interface rendering markdown, a coding agent running a suggested command, a tool call built from generated arguments, a summary fed into a second agent's prompt, or a webhook that forwards the text onward.
Solution
Enumerate the destinations before writing any filter. Every place model output lands and is interpreted — HTML renderer, shell, query, file path, URL fetcher, another agent's context, a downstream webhook — is a distinct boundary with distinct rules.
Encode at the destination, not at the source. HTML-escape for the renderer, parameterise for the query, pass an argv array to the process. The encoding belongs where the interpretation happens, because only there do you know what will be interpreted.
Prefer structured output over prose you have to parse back. A tool call with typed arguments has a schema to validate against; a sentence you regex for a filename does not.
Validate the value, not only the syntax. An encoded path is still a path: check it resolves inside the directory you meant before opening it.
Treat outbound URLs as a destination of their own. Rendered images and links cause a fetch with no click, so they carry data out; allowlist the hosts a rendered link may reach.
Make the boundary the only route. If any code path can consume raw model output without passing a destination encoder, the control is advisory rather than real.
Components
Benefits
- Breaks the injection chain where it matters: even a fully persuaded model cannot make a downstream system act, because that system never interprets its text.
- Independent of model behaviour. It keeps working across model upgrades, new jailbreaks and prompt changes, because it does not depend on the model refusing anything.
- Testable. Each destination has a payload and a pass or fail, so the control produces evidence rather than assurance.
- Cheap when applied early. Adding an encoder is a boundary change; retrofitting one after the destination is everywhere is a refactor.
Risks
- One sanitiser for every destination. It feels like a control, satisfies the checklist, and is wrong at every boundary except the one it was written for.
- Encoding that breaks the product: over-escaping turns legitimate markdown, code blocks and non-Latin text into noise, and the pressure to loosen it lands on the encoder rather than on the destination list.
- A destination inventory that ages. New integrations add consumers, and nothing fails when one is missed.
- Confusing detection with encoding. Scanning output for suspicious strings catches last year's payloads; encoding does not need to recognise the attack at all.
When not to use it
- Output that is never interpreted — a score, an enum, a boolean the caller compares. Constrain the type instead; an encoder on a closed value set is ceremony.
- Fully local single-user tools with no rendering and no process execution, where the only consumer is a person reading text.
- Where the destination already parameterises by construction, such as an ORM binding or a template engine that escapes by default. A second encoder gains nothing and can double-encode.
Technologies
Examples
- A support agent summarises a ticket whose body contains a markdown image pointing at an attacker's host. The console renders it, the browser fetches the URL, and the conversation leaks with nobody clicking anything. Disabling raw HTML and allowlisting image hosts closes it.
- A coding agent proposes a shell command. The runner passes the string to a shell, so a filename containing a command separator executes. Passing an argv array instead removes the shell's parsing step entirely.
- One agent's summary is placed into a second agent's prompt. The summary contained instructions, and the second agent followed them. Fencing the untrusted span and labelling it as data is the boundary in that case.
KPIs
- Destination coverage
- Share of known model-output consumers with an encoder at the boundary. Below 100% the control has a specific hole, and naming the destination is more useful than a percentage that averages it away.
- Payload neutralisation rate
- Share of per-destination test payloads neutralised at the boundary. The target is 100%: anything else names a destination to fix rather than a number to improve.
- Time to cover a new destination
- How long from a new integration going live to its encoder existing. Measures whether the inventory keeps up with the product rather than whether it was right once.
- Boundary trigger volume
- How often encoders neutralise something in production. A flat zero usually means the encoder is not on the path, not that nothing hostile is arriving.
Observed failure modes
- Silent exfiltration through rendered markup: an image or link causes a fetch without a click, so data leaves with no user action and no error anyone would notice.
- Second-order injection: output encoded correctly for the console is stored and later rendered somewhere else — a log viewer, a ticket, a digest email — where that encoding does not apply.
- The encoder that only sits on the happy path. Error branches, retries and fallbacks emit the same text through a different code path with nothing on it.
- Double encoding. Two layers each escape correctly, users see escaped entities in the product, and the fix removes the wrong layer.
Lessons learned
- Enumerate destinations before writing filters. Almost every real failure here is a consumer nobody listed, not an encoder that was written wrong.
- The destination you forget is rarely a screen. It is a webhook, a log viewer, an export or a digest email — somewhere the output goes without anyone thinking of it as a render.
- Encoding beats detection because encoding does not need to recognise the attack. A payload blocklist is a description of the attacks that were already public.
- Do not let a single sanitise() become the answer. The name suggests completeness and the behaviour is correct for exactly one destination.
FAQs
- Isn't this the same as filtering inputs for prompt injection?
- No — they defend opposite ends. Input filtering tries to stop the model from being persuaded, which depends on the model. This assumes persuasion already succeeded and stops the output from being acted on, which does not depend on the model at all. A system with only the first fails the moment a new jailbreak appears.
- Isn't this just output sanitization? Why not one sanitiser for everything?
- Because encoding is contextual. Escaping a quote protects an HTML renderer and does nothing for a shell; shell-quoting protects a shell and corrupts displayed text. A shared function has to choose one context, is wrong in the others, and looks like coverage while being a single point of failure.
- The model is ours and the prompt is fixed. Do we still need this?
- Yes, if any content the model reads comes from outside: a fetched page, a user file, a tool result, a memory record. The instruction does not have to arrive through your prompt — it arrives through whatever the model reads, and your prompt being fixed does not constrain that.