BYOKchat Blog

How to Build Human Approval Into AI Tool Calls

Design AI tool approvals that show exact operations, bind consent to normalized arguments, support per-tool policies, and remain safe across retries, crashes, and multi-round workflows.

· 6 min read

On this page
  1. Approval belongs after parsing and validation
  2. Preview meaning, not protocol syntax
  3. Every tool should provide an approval renderer
  4. Approval should bind to normalized arguments
  5. Never let the model edit an approved action silently
  6. Per-tool policies are more useful than one global toggle
  7. “Always Allow” still requires validation and authorization
  8. Scope persistent approval narrowly
  9. Tool identity is more than the display name
  10. Schema changes can expand authority
  11. Classify tool effects
  12. Read-only still can be sensitive
  13. High-impact actions can require stronger confirmation
  14. Batch approvals need exact membership
  15. Parallel tool calls can wait on approval together
  16. Approval can outlive the foreground model request
  17. Approval should expire when context becomes stale
  18. Revalidate authorization after approval
  19. Denial should produce a structured tool result
  20. A denial is not a transient failure
  21. Cancellation and approval are different
  22. Idempotency starts before the approval card
  23. Persist the decision audit trail locally
  24. Do not expose approval tokens to the model
  25. MCP tools need the same client-side policy
  26. Provider-hosted tools need different approval mechanics
  27. Approval UX should explain data flow
  28. Avoid dark patterns
  29. A reference approval architecture
  30. Test approval integrity, not only buttons
  31. Where BYOKchat fits
  32. 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:

  1. validate every call;
  2. separate auto-allowed and approval-required calls;
  3. present approvals;
  4. 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

Diagram illustrating the surrounding section

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.

Further reading

Keep reading