Somewhere between "the assistant answers questions" and "the assistant can reschedule your booking," there's a threshold that doesn't get enough attention. Read access is forgiving: a wrong answer is embarrassing, but it doesn't move anything. Write access changes the calculus. The moment an agent can create, cancel, or reschedule something in a live system, every gap in the layer underneath it stops being theoretical.
I ran this pass on an agent build recently, right before widening what it was allowed to do, the same build I wrote about in connecting an assistant to live systems. What I found wasn't exotic. It was the kind of thing that's easy to leave in place precisely because it never gets exercised until it does.
A narrow vocabulary of actions, not an API key
The instinct when an agent earns write access is to hand it the same interface a developer would use: authenticate once, then call whatever endpoint the situation calls for. That's the wrong shape. A developer calling an API has judgment, and consequences outside the system, if they make a mistake. An agent calling the same API has neither. It will reach for whatever endpoint the conversation nudges it toward, just as confidently as it reaches for the right one.
The fix is to give it a short, named list of operations instead of a key to the whole system: check availability, hold a slot, confirm a booking, reschedule an existing and already-verified one, cancel it. Each of those is bounded, with its own rules about what has to be true before it can run. Notice what's missing from that list: creating a brand-new record where verifying who someone is matters stays on a form rather than a free-text conversation, because a form is a much harder place to talk your way past than a chat window.
It sounds like a small distinction, five named operations instead of open API access, but it changes what a mistake costs. If a model gets pushed somewhere it shouldn't, the damage is bounded by what those five operations can do, not by what the underlying system can do. An agent that can only call five things is safe by construction. An agent that can call anything is safe only by accident, for as long as nothing goes looking for the gap. It's the same principle behind OWASP's API Security guidance on function-level authorization: the fewer functions a caller can reach, the smaller the blast radius when something gets it wrong.
"It's just logging" doesn't stay true
A webhook endpoint that receives events and writes them to a log file is low risk today. The temptation is to leave it unauthenticated: it only reads, it doesn't act, and there's no obvious contract with the sender for how to verify the payload anyway.
The problem is that "just logging" describes what the endpoint does today, not a property of the endpoint itself. The day someone wires that event stream into an action, syncing a calendar, triggering a notification, kicking off an update elsewhere, the endpoint inherits write-adjacent trust it was never built to deserve. The right time to add signature verification is before that day, even if it means shipping a check that does nothing yet except wait for a secret to be configured.
Every open decision needs an owner, not a default
Not every gap gets fixed. Some get accepted, and that's a legitimate outcome of a security pass, not a failure of one. The difference between an accepted risk and an oversight is whether someone actually decided, and whether the mitigation is written down next to the decision, the same discipline behind keeping a real audit trail rather than just a transcript. I don't verify identity again at this step, because an earlier step in the flow already did, and here's the compensating control if that assumption turns out to be wrong: that's a design choice. The same gap with no owner and no compensating control is just a hole.
Writing that distinction down matters more than it sounds like it should. Six months later, nobody can tell the two apart by reading the code alone. Only the note next to it tells you whether the gap was a decision or an accident.
Do the pass before you widen the surface, not after
The instinct is to build the feature and harden it once it's real. I'd argue for the opposite ordering whenever an agent is about to get a broader interface than it had before: more endpoints, more actions it can take unsupervised, more systems it can reach. Every gap that exists before you widen the surface gets multiplied by however much bigger that surface becomes. Fixing five issues before adding a dozen new write paths is a smaller job than finding the same five issues scattered across seventeen paths afterward, some of which didn't exist the last time anyone looked.
None of this is about paranoia for its own sake. It's about matching the level of scrutiny to what the system is now trusted to do. An agent that answers questions earns trust by being accurate. An agent that acts on your systems has to earn a different kind of trust first, and that pass is worth doing on purpose rather than letting it happen by accident.
If your team is about to hand an AI build write access to something that matters, bookings, records, anything that used to require a human to approve it, that's the moment to have someone look hard at what's underneath before you flip the switch. That's the kind of pass I run.