Every team that connects an agent to a real system writes some version of the same sentence into their system prompt:
You may read from the CRM but you must never delete a record.
It works, most of the time. That is the problem. A rule that works most of the time is indistinguishable from a rule that works, right up until the afternoon it does not.
Why prompts make poor permissions
A system prompt is input to a model, and a model is a thing that weighs inputs against each other. Your instruction is competing with the user's message, with the tool descriptions, with whatever the last tool call returned — and it does not automatically win.
There is no need to invoke prompt injection to see the failure. Ordinary ambiguity is enough:
- A user asks the agent to "clean up the duplicate contacts". Deleting duplicates is the task. Which instruction should win?
- A tool is named
contacts.merge. It deletes one of the two records. Nothing in its name says so. - The conversation runs long, the early turns fall out of the window, and the rule goes with them.
None of those are adversarial. They are Tuesday.
What enforcement looks like instead
The gateway sits between the agent and the application, which means it sees the call after the model has decided to make it and before anything happens. That is the only place a rule can be checked rather than requested.
Each tool is classified from the verb in its own name. Tool slugs arrive as
TOOLKIT_VERB_OBJECT — GITHUB_CREATE_ISSUE, SLACK_SEND_MESSAGE — and the
verb is the whole signal: LIST, SEARCH and GET are reads; DELETE,
REVOKE, PURGE and OVERWRITE are destructive; everything else is a write.
You then set a rule per application:
| Application | Read | Write | Destructive |
|---|---|---|---|
| HubSpot | Allow | Allow | Block |
| Gmail | Allow | Allow | Block |
| GitHub | Allow | Allow | Block |
Each row is two-state: allow, or block. There is no "ask" — the gateway speaks stateless HTTP and has nowhere to put a question, so the answer has to be decided before the call rather than during it. That constraint is worth understanding rather than working around: a rule that can be answered under time pressure by whoever is watching is not really a rule.
"This agent may read anything and delete nothing" becomes one rule instead of four hundred. Blocked tools are handled twice over: they are left out of the tool list the agent is given at all, and refused again if one is called anyway. The refusal names the rule, so the agent can report it to the person who asked rather than silently retrying — or worse, succeeding.
The honest caveat
Two of them, actually.
Classification is inference over a verb, not a certified inventory. It will meet tools it cannot place.
Those are treated as writes. That is a deliberate choice about which way to be wrong: a misfiled read is an annoyance, and a misfiled destructive action is an incident. Treat the classifier as a broad guardrail — a good one — rather than as proof.
The second caveat is about where the rules live. Tier rules are part of Enhanced Control, which means a plan that includes it and a workspace admin switching it on. With it off, the editor still saves your rules and tells you plainly that they are not being enforced. Rules you set on a specific app or a specific tool apply on every plan, so a downgrade can never quietly unblock something you explicitly blocked.
Where to start
If you have exactly one afternoon, spend it on the destructive column. Set every application your agents can reach to block destructive actions, then relax it where you have a reason to. It is a smaller change than it sounds, and it moves the sentence out of your prompt and into a place that enforces it.
Then read your activity log. Every call an agent makes is a row there — the tool, the outcome, how long it took — and the blocked ones are rows too. A week of that tells you which rules are doing work and which ones you guessed wrong.