On this page
- Approval belongs after parsing and validation
- Preview meaning, not protocol syntax
- Every tool should provide an approval renderer
- Approval should bind to normalized arguments
- Never let the model edit an approved action silently
- Per-tool policies are more useful than one global toggle
- “Always Allow” still requires validation and authorization
- Scope persistent approval narrowly
- Tool identity is more than the display name
- Schema changes can expand authority
- Classify tool effects
- Read-only still can be sensitive
- High-impact actions can require stronger confirmation
- Batch approvals need exact membership
- Parallel tool calls can wait on approval together
- Approval can outlive the foreground model request
- Approval should expire when context becomes stale
- Revalidate authorization after approval
- Denial should produce a structured tool result
- A denial is not a transient failure
- Cancellation and approval are different
- Idempotency starts before the approval card
- Persist the decision audit trail locally
- Do not expose approval tokens to the model
- MCP tools need the same client-side policy
- Provider-hosted tools need different approval mechanics
- Approval UX should explain data flow
- Avoid dark patterns
- A reference approval architecture
- Test approval integrity, not only buttons
- Where BYOKchat fits
- Further reading
“Ask before running tools” sounds safe until the approval UI says only:
Allow tool call?
[Allow] [Deny]
That does not tell the user what they are authorizing.
A useful approval system must answer:
What will happen?
Which data/resource is affected?
Which tool/server is responsible?
What arguments will be used?
Is the action reversible?
Will this approval apply once or in the future?
Human approval is a product-security boundary, not a decorative modal.
Approval belongs after parsing and validation
Do not ask users to approve raw streamed JSON fragments.
The safe order is:
model emits complete call
→ parse
→ schema validate
→ normalize
→ authorization preflight
→ build semantic preview
→ ask user
→ execute exact approved operation
If the call is malformed or unauthorized, reject it before involving the user.
Preview meaning, not protocol syntax
Bad approval:
{
"name": "send_email",
"arguments": "{\"to\":[\"alex@example.com\"],...}"
}
Better:
Send email
To: Alex <alex@example.com>
Subject: Release schedule
Body: 824 characters
This will send immediately.
The approval card should be generated by trusted application code from validated arguments.
Every tool should provide an approval renderer
A generic JSON viewer is useful as an advanced detail, but each meaningful side-effect tool should define a human-readable preview.
For example:
protocol ToolApprovalPreviewing {
func approvalPreview(for args: NormalizedArguments) -> ApprovalPreview
}
Possible preview fields:
title
summary
targets
important changed values
risk/reversibility note
server/tool identity
advanced raw arguments
This scales better than hard-coding tool names in the chat UI.
Approval should bind to normalized arguments
Suppose the model requests:
{
"to": ["alex@example.com"],
"subject": "Hello",
"body": "Hi"
}
The application validates and normalizes that request, then asks the user.
The approval should bind to the exact normalized representation, for example via a digest:
approvalID
+ tool identity/version
+ normalized arguments hash
+ operation ID
If anything changes afterward, require new approval.
Never let the model edit an approved action silently
Unsafe sequence:
user approves $50 payment
model changes args to $500
executor uses old approval
The executor must compare the operation being executed with the approved digest.
Approval is for this operation, not for the abstract tool name.
Per-tool policies are more useful than one global toggle
A client may expose:
Disabled
Ask
Always Allow
per tool or server/tool pair.
Examples:
weather lookup → Always Allow
read project file → Ask or Always Allow depending on user
send email → Ask
payment → Ask, possibly never eligible for Always Allow
The tool registry can declare which policy choices are permitted.
“Always Allow” still requires validation and authorization
It means:
skip interactive approval prompt for future eligible calls
It does not mean:
accept malformed arguments
authorize other tenants
ignore path restrictions
bypass rate limits
All non-human safety checks remain active.
Scope persistent approval narrowly
“Always allow” can be scoped to:
this exact tool
this MCP server + tool
this project
this chat
this account/workspace
this session
Broader scope is more convenient but grants more authority.
A good default is to bind persistent policy to the stable tool identity and the connection/server that owns it.
Do not automatically transfer approval from one server to a different server that exposes a tool with the same name.
Tool identity is more than the display name
Two servers can both expose:
search
create_issue
send_message
Treat identity as something like:
connection/server ID
+ tool name
+ tool schema/version fingerprint
If the schema changes substantially, consider invalidating or reviewing stored persistent approval.
Schema changes can expand authority
Suppose a previously read-only tool schema becomes:
search(query)
→ later:
search(query, delete_after_reading)
A stored “Always Allow” policy from the old schema may no longer be appropriate.
Track a schema fingerprint or declared permission class and require re-consent when authority expands.
Classify tool effects
Approval UX can use a trusted effect classification:
read-only
reversible write
irreversible/high-impact write
This classification comes from the tool registry/server policy—not the model.
It can drive:
- whether approval is mandatory;
- whether persistent allow is permitted;
- warning language;
- confirmation friction.
Read-only still can be sensitive
A read tool can access:
- private messages;
- local files;
- contacts;
- medical/financial records;
- internal company docs.
“Read-only” means it does not mutate state, not that it is harmless.
Approval policy should also consider data sensitivity and scope.
High-impact actions can require stronger confirmation
For some operations, one generic Allow button may be insufficient.
Examples:
transfer money
delete production resources
publish content publicly
send to many recipients
Possible product controls:
- require biometric/system confirmation;
- require typing a resource name;
- disallow persistent “Always Allow”;
- show before/after diff;
- require a second trusted application step.
The model should never be able to reduce this friction.
Batch approvals need exact membership
If a model requests five independent calendar events, a grouped card can be better UX:
Create 5 events?
1. Design review — Mon 10:00
2. Launch check — Tue 09:00
...
But approval should bind to all five exact calls.
If one call changes or a sixth is added, that part needs a new approval.
For heterogeneous/high-risk calls, separate cards may be clearer.
Parallel tool calls can wait on approval together
A scheduler can:
- validate every call;
- separate auto-allowed and approval-required calls;
- present approvals;
- run approved independent calls concurrently under limits.
For side-effect batches, consider whether auto-running some calls before the user resolves another approval creates confusing partial state.
The correct policy depends on tool effects.
Approval can outlive the foreground model request
On mobile, the app may be suspended while waiting for the user.
Persist:
operation ID
tool call ID
normalized arguments
approval preview
approval state
created time
expiry
Do not depend on an in-memory continuation remaining alive.
Approval should expire when context becomes stale
A pending action may no longer be appropriate hours later.
Examples:
send a message based on old conversation state
modify a record that changed
book a time slot that expired
Tools can declare an approval TTL or revalidation rule.
On resume, re-check semantic preconditions before execution.
Approval does not freeze external reality.
Revalidate authorization after approval
User permission can change while approval is pending.
Before execution:
verify approval integrity
→ verify current authorization
→ verify current preconditions
→ execute
Do not assume authorization from the time the card was created remains valid forever.
Denial should produce a structured tool result
If the user denies a call, tell the model semantically:
{
"ok": false,
"error": {
"code": "user_denied",
"message": "The user did not approve this action."
}
}
The model can then:
- propose a safer alternative;
- ask the user a question;
- stop.
Do not retry the same denied action automatically.
A denial is not a transient failure
Your retry system should classify:
user_denied
as non-retryable unless the user explicitly changes their decision or initiates a new operation.
Blindly asking again becomes coercive and annoying.
Cancellation and approval are different
If the user presses Stop while an approval card is pending:
cancel parent operation
→ mark approval obsolete
→ prevent later execution
The stale card should not remain actionable after the operation has been cancelled.
Idempotency starts before the approval card
Create the tool execution identity before asking approval.
Then:
approval binds to execution ID
execution ID binds to idempotency key
If the app crashes after approval but before the side effect, recovery can resume safely with the same identity.
See Idempotency for AI Tool Execution.
Persist the decision audit trail locally
Useful records include:
tool identity
operation ID
requested at
approved/denied at
policy source: Ask / Always Allow / Disabled
argument digest
execution result
Avoid storing sensitive raw arguments when a digest plus semantic record is enough, depending on your product’s audit requirements.
Do not expose approval tokens to the model
The model does not need the internal approval ID, signature, or authorization token.
Those are trusted application data.
The model only needs the resulting tool success/denial state.
Keeping trust metadata out of model context reduces attack surface.
MCP tools need the same client-side policy
A remote MCP server can expose a tool schema, but that does not mean every connected model should call it freely.
The client can apply:
Disabled
Ask
Always Allow
per MCP tool while still following MCP protocol semantics for invocation/results.
Protocol capability and user authorization are separate layers.
Provider-hosted tools need different approval mechanics
Some model providers execute hosted tools internally.
The client may not receive a pre-execution function call it can intercept in the same way.
For those tools, approval/control may need to happen at request configuration time:
Enable web search for this chat?
Allow URL retrieval?
Do not claim per-call approval if the provider protocol does not expose a pause point.
See Hosted Tools vs Client-Executed Tools.
Approval UX should explain data flow
For remote tools, show where data goes when meaningful:
This tool is provided by “Company MCP Server” and will receive:
- search query
- selected project ID
Users cannot make informed choices if every tool appears as a local action.
Avoid dark patterns
Approval controls should not:
- visually emphasize Allow while hiding Deny;
- repeatedly re-prompt after denial;
- make “Always Allow” the easiest accidental choice;
- obscure irreversible effects;
- label a write as “Continue” without explaining the action.
Trust depends on clear, symmetric choices.
A reference approval architecture
Test approval integrity, not only buttons
Important tests:
arguments change after approval
schema changes after persistent allow
approval card restored after app restart
approval expires
authorization revoked while pending
user cancels parent operation
batch approval membership changes
same tool name on different server
Always Allow still rejects invalid args
user denial is not retried automatically
crash after approval before execution
These are the scenarios that determine whether approval is actually a security control.
Where BYOKchat fits
A multi-provider client can keep one provider-neutral permission system across local tools and MCP tools while respecting provider-hosted tool limitations. Each tool has a stable identity and policy; approval cards are generated from validated arguments and bound to durable execution state.
That gives users meaningful control without making every safe read require repetitive confirmation.