Skip to content
Preptima
hardDesignScenarioSeniorStaffLead

You are shipping an agent that can call tools. How do you stop it exfiltrating data?

Assume the agent's context can be manipulated and control the exits instead. Scope each tool to the least data it needs, resolve the caller's identity in the executor rather than from a model-supplied argument, treat generated arguments as hostile input, and allow-list egress destinations.

5 min readUpdated 2026-07-28
Practice answering out loud

What the interviewer is scoring

  • Whether authorisation is placed in the tool executor rather than described as a prompt instruction
  • Does the candidate enumerate exfiltration channels beyond an obvious HTTP call
  • That tool arguments produced by the model are treated as untrusted user input
  • Whether the answer covers what is logged and detectable, not only what is blocked
  • Can they explain why a broadly scoped read tool is more dangerous than a narrowly scoped write tool

Answer

Start from the assumption you cannot win in the prompt

An agent's context is assembled from things you do not control: the user's message, retrieved documents, tool results, web pages, other people's data in a shared workspace. Any of those can carry instructions, and the model has no enforced way to tell an instruction from data. So the design question is not "how do I stop the model being told to leak" but "if the model decides to leak, what is physically available to it".

That reframing is what the interviewer is listening for. Everything useful follows from treating the model as a component that emits plausible-looking tool calls, some of which are attacker-influenced, and putting the controls in the code around it.

Authorisation belongs in the executor

The most consequential decision is where identity is resolved. If the model produces get_document(id, user_id) and your tool trusts user_id, the model can read anyone's documents by writing a different number, and no injection cleverness is required. The correct shape is that the executor holds the session's principal, derives credentials from it, and ignores any identity the model supplies. Tenancy, row-level filters and scopes are applied server-side on every call, exactly as they would be for an HTTP endpoint, because that is what a tool is.

# The model supplies what to fetch. It never supplies who is asking.
def get_document(doc_id: str, *, principal: Principal) -> Document:
    doc = repo.find(doc_id)                  # principal comes from the session,
    if not authz.can_read(principal, doc):   # not from the model's arguments
        raise Forbidden(doc_id)
    return doc

Every tool then gets its own scope rather than inheriting a service account. A summarisation tool needs read access to one document, not to the corpus. A calendar tool needs the user's calendar, not the organisation's directory. This is tedious and it is the work: an agent with one broadly privileged credential has a blast radius equal to that credential, whatever the prompt says.

Scope the write side by irreversibility rather than by importance. Actions that can be undone can be autonomous; actions that spend money, send communications outside the organisation, change permissions or delete data require explicit confirmation, with the real arguments shown to the human rather than the model's summary of them.

Enumerate the exits

Exfiltration needs a channel, and candidates typically name one. The value is in naming the ones that do not look like network calls.

The rendered response is a channel. If your UI renders markdown from model output, an image reference causes the user's browser to fetch an attacker-chosen URL with whatever the model encoded into the path or query string, and the user clicks nothing and sees nothing. Links are the same attack with one click of social engineering. Sanitise rendered output: allow-list image and link hosts, or strip remote references entirely.

Any tool with a caller-supplied destination is a channel. An HTTP tool, a webhook sender, a "share this document" tool, an email or chat integration where the recipient is an argument. So is a code interpreter with network access, and so is a file write into a location that is served publicly. Even a search tool can be a low-bandwidth channel if queries are logged somewhere the attacker can read.

Egress control is the general answer. The agent's runtime, and especially any sandbox running generated code, should have no default outbound network route; specific destinations are allow-listed, everything else is refused and logged. For tools where a destination is a legitimate parameter, constrain the domain set rather than the URL, and require confirmation when the destination is outside the organisation. Refusals are a signal — an agent attempting a blocked destination is either a bug or an attack, and both are worth an alert.

Limit what is in the context to begin with

Data that never enters the context cannot leave through the model. This is the cheapest control available and it is routinely skipped because it constrains the product. Retrieve the minimum: passages rather than whole records, the fields the task needs rather than the row. Redact identifiers, contact details and secrets at the retrieval boundary when the task does not need them. Where the agent must reference something sensitive, hand it a token that the executor resolves at call time so the value itself is never in the sequence — the same reasoning as a payment card token.

The dangerous configuration to name explicitly is one agent holding untrusted content, private data and an outbound channel simultaneously. Splitting that across components with a validated seam between them is a stronger control than any amount of instruction hardening, because the component reading hostile text has no private data to give away and the component holding private data never reads hostile text.

Assume something gets through, and be able to see it

Prevention is incomplete, so the second half of the answer is detection and forensics. Log every tool call with the principal, the resolved arguments, the decision made by the authorisation check and the size of the response, and keep the trace linked to the conversation that produced it. That is what lets you answer, after an incident, exactly which records were read and by which session, which is the question you will be asked and the one teams usually cannot answer.

Then alert on behaviour rather than content. A sharp rise in records read per session, a first-ever destination domain, an unusual ratio of reads to user turns, repeated authorisation failures against sequential identifiers: these are the same anomaly signals you would use for a compromised service account, because that is functionally what a captured agent is. Rate-limit and cap per session so that the worst case is bounded, and make the cap low enough that a slow drip hits it before it matters.

One thing worth stating because it is often assumed away: none of this depends on the model being adversarial. A model that is merely confused, or following a stale instruction from earlier in a long conversation, produces the same tool call as a compromised one. Controls placed in the executor cover both cases; controls placed in the prompt cover neither reliably.

Likely follow-ups

  • The agent renders markdown in your UI. Why is that an exfiltration channel and what do you do about it?
  • How do you scope a search tool that legitimately needs to query across tenants?
  • Your agent runs code in a sandbox. What does the sandbox still need to prevent?
  • How would you detect a slow exfiltration spread across many normal-looking requests?

Related questions

Further reading

agentsexfiltrationleast-privilegeegress-controlllm-security