Somewhere right now, an AI agent has a standing API key with more access than the intern you'd never hand your prod credentials to. It can read your repos, send emails on your behalf, hit paid APIs, maybe even push code, and it's making those calls based on a model's best guess at what you meant, not a human clicking "confirm."
That's not a hypothetical.
As agentic workflows have gone from novelty to daily tooling, the security model most people are using hasn't caught up: copy a long-lived key into an environment variable, give the agent a wide set of tools, and hope the prompt is good enough to keep it in line. It usually is...until it isn't.
I'm trying my absolute best to not make this post another
"agents are the future" piece.
It's a practical look at the specific failure mode a lot of teams are quietly worried about. Over-permissioned agents, and what to actually do about it: scoping access, avoiding standing credentials, and putting a human back in the loop for anything irreversible.
Traditional app security has a comforting property: you wrote the logic. If your backend calls the payments API, you know exactly which code path triggers that call, and you can trace every request back to a line you (or a teammate) typed on purpose. The attack surface is what you built.
Agents break that assumption. You're not authorizing a fixed set of actions anymore — you're authorizing a decision-maker. The agent reads a prompt, forms an interpretation of what you want, and then reaches for whatever tools are in scope to get there. Most of the time that interpretation is reasonable. But "most of the time" is a strange thing to build a permissions model around when the downside is a deleted repo or a $400 API bill.
you ask an agent to "clean up old test data before the demo." It has database access, because of course it does — that's the job. It interprets "old" a little more aggressively than you meant, or it decides a table looks like test data when it isn't, and now you're restoring from backup an hour before the demo. Nobody wrote a bug. Nobody approved that specific deletion. The system did exactly what it was authorized to do; it just chained together a decision nobody explicitly signed off on.
That's the real shift: in traditional software, permissions bound code. With agents, permissions bound judgment — and judgment, unlike code, doesn't produce the same output twice. A key that's fine to hand to a deterministic script is a very different thing to hand to something that improvises.
The fix isn't "don't give agents access" — that defeats the point. It's shrinking the blast radius so that when the agent's judgment goes sideways, the damage is small and reversible instead of catastrophic.
Stop handing out standing keys. A static API key sitting in an environment variable is a permanent, unmonitored trust decision you made once and then forgot about. Short-lived, scoped tokens change the math: if a token expires in fifteen minutes and only covers the one action the agent actually needs right now, a bad decision has a ceiling on how bad it can get.
Scope per tool, not per agent. It's tempting to give an agent "database access" as one blanket grant, the same way you'd hand a new hire a laptop and a single login. But an agent that only needs to read order history doesn't need write access to the whole schema. The narrower the grant, the fewer ways a misinterpreted instruction turns into an incident.
Put a human back in the loop for anything you can't undo. This is the one that's easy to skip because it adds friction — but friction is the point. Deletions, financial transactions, outbound emails to real customers: these deserve a confirmation step, even if it slows the agent down. You're not trying to make the agent slower everywhere, just at the handful of points where "oops" isn't recoverable.
Log every tool call like you mean it. When something does go wrong, the question won't be "did the agent make a mistake" — it will be "what exactly did it do, and in what order." Without a clear audit trail, you're debugging a black box after the fact. With one, you can trace the decision back to the exact prompt and permission that allowed it.
None of this is exotic. It's the same least-privilege thinking that's underpinned good infra security for years — it just hasn't fully made its way into how people wire up agents yet, because the tooling is new enough that "give it broad access and see what happens" is still the path of least resistance.
agent = Agent(
tools=[
DatabaseTool(api_key=DB_MASTER_KEY, access="read-write"),
EmailTool(api_key=EMAIL_API_KEY, access="send"),
PaymentsTool(api_key=PAYMENTS_KEY, access="charge-and-refund"),
],
system_prompt="You're a helpful assistant for our support team."
)
agent.run("Clean up stale test accounts and notify the affected users.")
Every tool here is fully authorized, indefinitely, with no ceiling on scope. The agent can delete any row in any table, email any address, and issue refunds — all because "clean up and notify" sounded like it needed some database and email access, so it got all of it. The key never expires. Nobody re-checks this grant next month, or next year.
agent = Agent(
tools=[
DatabaseTool(
token=issue_scoped_token(table="test_accounts", access="read-write", ttl_minutes=15),
),
EmailTool(
token=issue_scoped_token(scope="transactional-only", ttl_minutes=15),
requires_approval_for=["send"],
),
],
system_prompt="You're a helpful assistant for our support team."
)
agent.run("Clean up stale test accounts and notify the affected users.")