Prompt injection is not the vulnerability. Your agent’s tools are.
Prompt injection will get through. AI agent security depends on what the agent can do when it does: break the lethal trifecta and enforce limits in code.
Most teams building AI agents ask: how do we stop prompt injection?
We at Vstorm do not think that is the right question. The better question is: what can the model actually do when an injection inevitably succeeds?
The problem #
LLMs follow instructions. This is what makes them so useful: we feed them instructions in human language and they do our bidding.
The problem is that they do not just follow our instructions. They will happily follow any instructions that make it to the model. “Summarize this document” invites arbitrary text into the model’s context, and the model has no reliable way to distinguish between your trusted instructions and untrusted data. Everything eventually gets glued together into a single sequence of tokens.
Because of this, trying to detect and filter malicious prompts is a losing game. The defender has to be right every time, but the attacker only needs to get lucky once. A 99%-effective classifier might give you useful telemetry, but it is useless as a security boundary if a single bypassed string gives an attacker access to your database.
We have seen this exact failure pattern ship in Slack AI and the GitHub MCP server. Invariant Labs’ write-up of the GitHub vulnerability gets to the core of the issue: it is “not a flaw in the GitHub MCP server code itself, but rather a fundamental architectural issue that must be addressed at the agent system level.”
An agent worth attacking #
Consider a standard research agent. It reads a prospect’s website, pulls context from your CRM, drafts an account brief, and emails it to the account owner.
It has four tools: web_search, http_get, crm_query, and send_email. The CRM tool takes raw SQL and runs on a service account. The mail tool accepts a recipient and a free-form body.
This is the lethal trifecta: access to untrusted content, access to private data, and the ability to communicate with the outside world. Individually, these capabilities are fine. Combined, they are fatal.
The exploit is trivial. A hidden instruction on the prospect’s website tells the agent to query the CRM, encode the results into a URL, and fetch it with http_get. The request lands on an attacker-controlled server with your customer data in the query string. The agent complies. Nothing in the system blocks the action because nothing in the system was designed to verify it.
The injection only opened the door. The damage came entirely from the tools the agent was already holding.
Assume it already happened #
Assume compromise is inevitable. A malicious prompt has made it into the context window. Your next step is to audit exactly what the agent is capable of doing at that moment.
Do not just look at the tool list; look at the credentials executing those tools. crm_query sounds like a narrow tool, but if the underlying service account has full read access to your entire customer database, that is exactly what the attacker has access to. Nobody intentionally grants full read permissions to an agent that “only summarizes,” but it happens constantly when teams move fast.
Next, look at exfiltration. Anything that moves bytes outward counts. This includes emails, webhooks, outbound HTTP calls, and even Markdown image URLs that your client fetches when it renders a response. If you leave a single exfiltration vector open, an attacker will use it.
Finally, evaluate the writes. Which of them are reversible, and would anyone notice if they were triggered maliciously at 3 AM on a Sunday?
Constraints, and what they cost #
Security should not rely on asking the model whether an action is safe. You need structural constraints in code. Each comes with real trade-offs.
Break the trifecta. Cut off one of the three legs. A research agent needs to read the web and internal documents, but it rarely needs to decide where its requests go. Any URL the model controls is an outbound channel, because data can ride along in the path or the query string. Let the system decide what gets fetched, deny all other egress, and entire classes of exfiltration attacks disappear. The cost is capability: the agent legitimately cannot do certain things anymore.
Make the tool signature the policy. Never ship run_sql(). Ship get_orders_for_customer(customer_id). Hardcoded parameters are a boundary a model cannot talk its way around. The trade-off is tool bloat. You have to write more tools, and your engineering team becomes the bottleneck for every new feature.
Separate proposing from executing. Route the model to draft_email(), not send_email(). But be careful here: human approval is often just security theater. If a human reviews 60 proposals a day, they become a rubber stamp within a week. Approvals only work if they are rare and the UI clearly shows what the agent is trying to do and why.
Authorize deterministically. The model proposes; a deterministic system decides. If the agent wants to update a customer record, a non-LLM layer has to verify whether that specific agent, acting for that specific user, actually has permission. Karol Piekarski describes that layer, the agentic AI control plane, in AI agents can act: can you still control them?
The same agent, rebuilt #
Every one of these fixes is standard software architecture.
crm_query becomes a strict API endpoint for specific fields instead of a raw SQL pipe. The prospect’s website still gets read, but the model no longer chooses what to fetch: the URL comes from the CRM record, and the system retrieves the page before the model ever runs. send_email and http_get are deleted entirely. The sandbox denies outbound network traffic by default, meaning a compromised model trying to ping an external server is blocked at the network level.
Now run the exact same attack on this rebuilt architecture.
The model can read the brief fields for its assigned account, but it cannot dump the CRM. It cannot send an email or make an HTTP request because those tools and network paths do not exist. It can write hostile text into a draft, but drafts are rendered and sent as plain text, so nothing in them can trigger a request when someone opens them. What is left is text, which is exactly why a human reviews the draft before it goes anywhere.
The attacker cannot steal the customer database. The model may still follow the injection; the data simply has no path out of the system.
That is the difference between a mitigation and a boundary. A mitigation asks the model to behave. A boundary does not care whether it does.
The boring but effective reality #
Least privilege, capability security, sandboxing, and scoped credentials are among the strongest ideas in security, and they are decades old. They were developed for exactly this scenario: managing a component you cannot fully trust inside a system that has to keep working anyway.
It is tempting to treat LLM agents as a wholly new category that requires novel defenses, such as guardrails that use models to monitor models. But that just makes your security recursive and fragile.
The older answer is boring, but it works: enforce your boundaries around capabilities and data flows in code, completely outside the model’s reach. The model will eventually be tricked. Your architecture is what determines whether that failure results in a rejected draft or an actual incident.
Sources #
Incidents and concepts cited in the text
- PromptArmor, Slack AI data exfiltration from private channels via indirect prompt injection (the Slack AI case), August 20, 2024
- Invariant Labs, GitHub MCP Exploited: Accessing private repositories via MCP (the GitHub MCP case and the architectural-issue quote), May 26, 2025
- Simon Willison, The lethal trifecta for AI agents: private data, untrusted content, and external communication (the three capabilities), June 16, 2025


