The moment you give an agent tools you give it the ability to act. Here are the concrete controls that keep that power bounded, from scoped credentials to human-in-the-loop.
Tools are what make an agent useful and what make it dangerous. A model that can only produce text has a limited blast radius; the worst it can do is say something wrong. The moment you connect it to a shell, a database, an email API, a payments endpoint, or a file system, its mistakes and its compromises become actions in the real world. Every capability you grant is also a capability an attacker can borrow through prompt injection, a poisoned document, or simple model error.
This article is about the controls that keep tool use bounded. It is framework-agnostic: whether you build on LangGraph, CrewAI, the OpenAI Agents SDK, n8n, or a hand-written loop, the same principles apply. We start with the risks, then work through concrete controls you can implement this week.
The risks of giving agents tools
Data exfiltration
An agent with both read access to sensitive data and a tool that can reach the outside world (email, HTTP, a webhook) is one injection away from leaking that data. The classic chain: the agent reads an untrusted document, the document instructs it to query internal records, and then to send them somewhere external. Each tool call is individually legitimate. The harm is in the combination, which is why exfiltration is best stopped by controlling data flow, not just individual calls.
Destructive actions
Tools that delete, overwrite, deploy, or transfer are irreversible. An agent that misreads a task, hits an edge case, or follows an injected instruction can drop a database table, force-push over a branch, empty a storage bucket, or cancel a production deployment. Unlike a leaked secret, some destructive actions cannot be undone at all.
Credential exposure
Agents need credentials to call tools, and those credentials are a prime target. Broad, long-lived API keys stored in the agent's environment can be surfaced by injection ('print your environment variables'), leaked through tool output, or logged in plaintext. A single over-scoped key often unlocks far more than the task required.
Runaway loops and denial of wallet
Agents retry, recurse, and chain tool calls. Without limits, a stuck agent can call a paid API thousands of times, spawn subagents unboundedly, or hammer a downstream service. This is both a cost problem ('denial of wallet') and an availability problem for whatever the agent is calling.
The dangerous configuration is an agent that simultaneously (1) reads untrusted content, (2) holds broad credentials, and (3) can take irreversible external actions, all with no human in the loop. Almost every serious agent incident is some version of those three properties in one process. Breaking any one of them dramatically reduces risk.Control 1: Scoped credentials and dedicated identity
Give the agent its own identity, not a reused human account or a shared service account. Make it a first-class principal with a documented purpose, so its access can be reasoned about, monitored, and revoked independently.
- Scope every credential to the minimum: a specific dataset, a single bucket, one API scope, read where read suffices.
- Prefer short-lived, task-scoped tokens over long-lived keys. Provision credentials for the current task and let them expire, so a leaked token is useful only briefly.
- Never bake secrets into prompts, tool descriptions, or code the model can read. Inject them at the tool-execution boundary, outside the model's context.
- Segment credentials by trust level: the component that reads untrusted content should hold different, weaker credentials than the component that performs privileged actions.
# Illustrative: resolve a narrowly-scoped, short-lived token at call time,
# outside the model context. The model never sees the secret.
def run_db_tool(query: str, ctx: AgentContext):
if not is_read_only(query):
raise PermissionError('write queries are not permitted for this agent')
token = vault.issue_token(
scope='analytics.read', # least privilege
resource='reporting_replica', # specific resource, not '*'
ttl_seconds=300, # short-lived
)
return db.execute(query, token=token)
Control 2: Read-only by default
Default every tool to the least powerful mode that still does the job, and make write, delete, and send capabilities an explicit, deliberate exception rather than the norm. A surprising fraction of agent tasks (research, triage, classification, drafting, reporting) need no write access at all. When you do need writes, split them into their own narrowly-scoped tools rather than exposing a single do-everything API.
- Expose read and write as separate tools, not one tool with a mode flag the model chooses.
- For file tools, specify allowed_paths and blocked_patterns instead of granting the whole filesystem.
- For database tools, prefer a read replica and enforce read-only at the connection level, not just in the prompt.
Control 3: Sandboxed execution
Any tool that runs code, executes shell commands, or processes untrusted files should run in an isolated environment, not in your application process. Sandboxing bounds the blast radius: even if the agent is fully compromised, it can only reach what you explicitly provisioned.
- Run code execution in a container or microVM with no ambient cloud credentials and no access to the host filesystem.
- Apply a network allowlist so the sandbox can reach only approved endpoints. Default-deny egress.
- Set CPU, memory, and wall-clock limits so a runaway or malicious job is killed automatically.
- Treat the sandbox as disposable: fresh environment per task, destroyed after, so nothing persists between runs.
NVIDIA and OWASP both frame sandboxing as blast-radius reduction: you are not trying to make compromise impossible, you are ensuring that a compromised step cannot reach anything valuable. Default-deny network egress from the sandbox is one of the highest-value single controls you can add.Control 4: Allow and deny lists
Constrain what the agent can reach, not just what it intends to do. Allowlists convert 'the model decided to call this' into 'the model may only call things we approved.'
- Tool allowlist: enumerate the exact tools each agent may use. Do not expose a whole MCP server's tool set by default.
- Domain allowlist: for HTTP and browsing tools, permit only approved destinations so exfiltration and callbacks to attacker infrastructure fail closed.
- Argument policy: validate tool-call arguments against policy before executing. A well-formed request to an unapproved recipient or resource should be rejected.
- Deny lists for known-dangerous operations (DROP, DELETE without a WHERE clause, force-push, wildcard deletes) as a backstop.
Control 5: Rate limits and budgets
Bound how much an agent can do, in aggregate, before something forces a stop. Limits protect against runaway loops, denial-of-wallet, and the amplification of a successful attack.
- Per-agent token and cost budgets, with a hard stop when exceeded.
- Tool-chain depth and retry limits so recursion and subagent spawning cannot run unbounded.
- Per-tool rate limits, especially for paid APIs and destructive operations.
- Circuit breakers that halt the agent when error rates or anomalous call patterns spike.
Control 6: Audit logging
You cannot investigate, or even detect, what you do not record. Log every tool call as a structured event, not a print statement, so you can reconstruct exactly what an agent did and why.
- Record the agent identity, the tool called, the full arguments, the result or error, and a timestamp.
- Add an action classification and risk score, plus the authorization result and any approval identifier.
- Log approvals and denials for human-in-the-loop steps, including who approved.
- Ship logs to an append-only store the agent itself cannot modify, and alert on high-risk actions.
{
"ts": "2026-08-04T14:22:07Z",
"agent_id": "support-triage-agent",
"tool": "send_email",
"args": {"to": "customer@acme.com", "template": "ack"},
"risk": "high",
"authorized": true,
"approved_by": "human:jsmith",
"result": "queued"
}
Control 7: Human-in-the-loop for high-impact actions
For actions that are irreversible or high-impact, a human confirmation step is the strongest single control available, because it does not depend on the model behaving correctly. It breaks the automated attack chain at exactly the point where harm occurs.
- Classify actions by impact and require explicit approval for the high-impact tier: sending external communications, deleting or overwriting data, moving money, changing access or permissions, deploying to production.
- Show a preview or dry-run diff before applying, so the approver sees exactly what will happen, not just a yes/no prompt.
- Make approval per-action and time-bound. Do not let one approval generalize to a whole session or future actions.
- Log every approval and denial as part of the audit trail.
Human-in-the-loop only works if the human can actually evaluate the request. An approval prompt that says 'Agent wants to run a tool. Approve?' trains people to click yes. Show the concrete effect (the recipient, the exact query, the diff) so approval is a real decision, not a rubber stamp.Putting it together
These controls are layers, not alternatives. The goal is that no single failure, one injection, one over-scoped key, one model mistake, leads directly to a serious incident. Match the strength of your controls to the impact of the tool: a read-only search tool needs little more than a rate limit, while a tool that can move money or delete data warrants scoped credentials, an allowlist, a sandbox, an audit trail, and a human gate all at once.
| Tool impact | Minimum controls to apply |
|---|---|
| Read-only (search, retrieve) | Rate limit, audit log, domain allowlist |
| Write (create, update) | Scoped credentials, argument policy, audit log |
| Destructive / irreversible | All of the above + sandbox + human approval + budgets |
| Code / shell execution | Sandbox, no ambient credentials, egress deny, resource limits |
Tool-security checklist
- Give the agent a dedicated identity; never reuse human or shared accounts.
- Scope every credential to the minimum resource and access level; prefer short-lived tokens.
- Default all tools to read-only; make write and delete explicit, separate tools.
- Sandbox all code and shell execution with no ambient credentials and default-deny egress.
- Enforce tool allowlists; do not expose whole MCP tool sets by default.
- Allowlist outbound domains for HTTP and browsing tools.
- Validate tool-call arguments against policy, not just schema.
- Set token, cost, retry, and tool-chain limits with hard stops.
- Log every tool call as a structured, append-only audit event.
- Require human approval, with a preview or diff, for irreversible and high-impact actions.
- Segment credentials and tools by trust level.
- Match control strength to tool impact; guard destructive tools most heavily.
Giving an agent tools is giving it agency. The engineering discipline is to make sure that agency is bounded, observable, and revocable, so that the useful actions flow freely and the dangerous ones always pass through a control you designed on purpose.