How do you stop AI agents accessing data they should not?
Scope the credential first, then add a runtime check inside the data-access tool itself. Scoping decides which systems an agent can reach at all. The runtime check decides whether this particular read, for this user, in this workflow, should happen now. It also inspects what comes back before it travels anywhere.
Both are necessary. Scope alone is static and coarse, and the risk is contextual.
Scope is necessary and insufficient
The first answer to this question is correct: scope the credential. Give the agent read access to exactly the tables and endpoints it needs and nothing more. Do that before anything else, because no runtime control compensates for an agent holding admin credentials.
Then notice what it does not cover. Inside the scope that you granted, the agent can still read a record it has no business reading for this task, combine data across permitted sources, or expose something in a response to the wrong user.
| Question | Answered by scope | Answered at runtime |
|---|---|---|
| Can this agent reach the customers table? | Yes | Not needed |
| Should it read 40,000 rows for a single support question? | No | Yes, with a limit on bulk reads |
| Is this record the one belonging to the user in session? | No | Yes, by checking arguments against the authenticated identity |
| Did protected fields just leave in the tool's response? | No | Yes, with detection on the output |
A check at the point of access
The complement to scoping is a runtime check in the data-access tool itself.
import { launchArcjet, tokenBucket, localDetectSensitiveInfo,} from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const bulkReads = tokenBucket({ refillRate: 20, intervalSeconds: 60, maxTokens: 60,});const pii = localDetectSensitiveInfo({ deny: ["CREDIT_CARD_NUMBER"] });
const volume = await arcjet.guard({ label: "tools.query-customer-data", actor: session.userId, correlationId: workflowRunId, rules: [bulkReads({ key: session.userId, requested: rowsRequested })],});
if (volume.conclusion === "DENY" || volume.hasFailedOpen()) { throw new Error(`Denied: ${volume.reason}`);}
const result = await queryCustomerData(args);
const output = await arcjet.guard({ label: "tools.query-customer-data", actor: session.userId, correlationId: workflowRunId, rules: [pii(JSON.stringify(result))],});
if (output.conclusion === "DENY" || output.hasFailedOpen()) { throw new Error(`Denied: ${output.reason}`);}A direct Guard call fails open (allow with error codes). The sample stops the tool when hasFailedOpen() is true. Vercel AI SDK and LangChain wrappers fail closed unless you opt into continuing on error.
At that point you know things the credential does not: which user the agent is acting for, which tool is running, what arguments it was given, and which workflow this belongs to. That is enough to enforce rules that a permission grant cannot express.
The actor must come from your authenticated server-side session, never from a model-supplied argument. A policy can be conditioned on the actor, so an agent that can set its own actor can select its own policy.
Check the way out
Reading data is not the breach. The breach is where it goes.
Run detection on the tool's output before it returns, as the second check in the preceding sample does. Protected fields then cannot travel into model context or a response by accident. This is the check most often missing, because the intuitive place to put a control is on the request rather than the reply.
Because Arcjet's sensitive-information detection runs in your application, you can inspect that output without exporting the very data you are protecting. For more information about that property, see keeping security inspection local. For the boundary-by-boundary version, see preventing data exfiltration through AI agents.
Validate the arguments
A limit on bulk reads catches the crude version. The precise version is checking that the arguments match the user that the agent is acting for.
If a support agent is handling a conversation with customer A, a call to fetch customer B's records is wrong regardless of volume, and the tool handler is the only place with both facts available. This is ordinary object-level authorization, and it is the top entry in the OWASP API Security Top 10. It ranks there for the same reason that it matters here: it needs application context that no perimeter layer has.
Write it as a plain check in the handler before the runtime check. Not everything needs to be a rule.
The contextual part
The hardest version of this problem is sequence-shaped. The agent read a customer list two steps ago, and now it is calling an export tool. Both are permitted. Together they are an exfiltration.
Enforcing on that pattern is what sequence-aware policy is for, and it is the direction that this field is moving. A control that checks each access in isolation approves both steps and works exactly as designed.
Two things help in the meantime. Tag decisions with a correlationId so that the workflow they belong to is recorded; that identifier does not change an allow or deny, but it makes the run reconstructable. Then identify the small number of dangerous combinations specific to your application. "Bulk customer read followed by any outbound tool in the same run" is a pattern that you can reason about in your own handler, and it does not require general sequence analysis.
For more information about the sequence problem, see anatomy of an agent incident and runtime controls on enterprise systems.
Where Arcjet fits
The identity layer scopes what an agent can reach. Products like Keycard, Aembit, and Astrix live there, and they are the right answer to "can this agent touch this system at all".
Arcjet is the layer below that question, inside the data-access tool, deciding whether this particular read should proceed and inspecting what comes back before it travels. It has the facts that a credential does not: the user in session, the tool running, the arguments supplied, and the workflow it belongs to.
The two are complements rather than alternatives. Scope first, then enforce at the access. Neither substitutes for the other, and a stack with only one of them has a predictable gap.
Learn more: Sensitive information detection ยท Arcjet Guards
Frequently asked questions
How do I stop an AI agent accessing data it should not?
Scope the credential first so the agent can only reach what it needs, then add a runtime check inside the data-access tool. The check has facts the credential does not: the authenticated user, the tool running, the arguments supplied, and the workflow it belongs to.
Why is scoping the credential not enough?
Scope is static and coarse while the risk is contextual. Inside the scope you granted, an agent can still read a record it has no business reading for this task, combine data across permitted sources, or surface something to the wrong user.
Should I check the tool's output as well as its input?
Yes, and the output check is the one more often missing. Reading data is not the breach; where it goes is. Run detection on what the tool returns before it travels into model context or a response.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Get allow, deny, and redact on agent actions before the side effect.