GuidesOpens this cluster

Confirm cards: letting an agent act without losing control

If the confirmation lives in the prompt it is a suggestion. Make it a branch in your server code, and decide the tier per tool rather than per sentence.

VVoqal · Engineering12 min read
An iOS confirm card for a money-moving action, showing a single payment-link total the user must accept before anything executes.

About this article

Speech recognition is good. It is not good enough to be the last thing that happens before money moves. Somewhere between a noisy room, a homophone and a model that filled in a plausible argument, there is a version of the request nobody made.

The failure is rarely dramatic. It is an amount off by a factor of ten, a recipient that matched the wrong contact, a date parsed in the wrong timezone. All of those are recoverable if the user sees them, and none of them are if the user only hears a sentence saying it is done.

A prompted confirmation is not a confirmation#

If the rule that the agent must ask before acting lives in the system prompt, it is a request, and requests get refused. A long conversation, an insistent user, or text written by a third party that the agent happened to read can all end with the model deciding this particular case is fine. Someone outside this industry wrote the sentence better than we have managed to: “human-in-the-loop is a security boundary, and security boundaries belong in code, not in prompts” (pavelgj on dev.to).

The empirical case is just as plain. The Berkeley Function Calling Leaderboard’s multi-turn analysis found that even the strongest models “sometimes fail to explore the current state before performing actions, which can be dangerous if the actions are non-reversible” (BFCL V3, 19 September 2024, updated 10 December 2024). And τ-bench, which tested agents given tools and written domain policies, found that “even state-of-the-art function calling agents (like gpt-4o) succeed on <50% of the tasks” (Yao et al., 17 June 2024). That figure describes GPT-4o in mid-2024 and models have improved since; the design conclusion has not changed, because the question is not how often the model is right but what happens on the occasion it is wrong.

There is no arrow from the model to execution in the lower path. Nothing the model can say produces one.

Where the boundary belongs#

The gap the card fills already exists in the protocol. When a model wants to use a tool it emits a tool-use block and stops: Claude “responds with stop_reason: "tool_use" and one or more tool_use blocks”, and “your code executes the operation and sends back a tool_result” (Anthropic tool use). The model proposes. Your runtime disposes. A confirm card is what you put in that gap.

The MCP specification takes the same position from the other direction, requiring that hosts “obtain explicit user consent before invoking any tool”, and warning that tools “represent arbitrary code execution” and that “descriptions of tool behavior such as annotations should be considered untrusted, unless obtained from a trusted server” (revision 2025-06-18).

Which tools get a card#

The split is declared once, per tenant, in configuration: this list of tool names changes state, everything else is read-only. The runtime consults that list before it executes anything. The model is not asked and cannot be talked into a different answer, which matters precisely because a tool description is an untrusted string.

When the model names a tool on that list, the call is held. The runtime does not execute it and does not let the model narrate a success. It builds exactly one confirm widget from the held arguments and sends that back alongside the spoken answer.

It is worth knowing where that lands in the turn. The SDK’s turn runs through five phases: idle, listening, transcribing while speech-to-text is in flight, thinking, and speaking. The confirm card arrives at the end of thinking and is on screen while the phase is speaking, which means the user is reading the arguments while the agent is still describing them. That overlap is deliberate. It also means an interruption during speaking has to leave the card alone, because the held call is not part of the audio state. That widget is part of the render spec, which is also where the rule lives that the agent may not draw a second card of its own. Two cards means the user approves the wrong one.

The held call and the approval are two separate requests. Nothing bridges them except a person.

Which tools land on the list is a decision about the tool, not about the sentence the user said, and it belongs next to the rest of the tool surface.

Four posts take a piece of this further, alongside the tiering question linked above. The adversarial case this gate exists for walks an injected instruction all the way to the spending path. The wider threat model covers what leaves the device and what your server has to refuse. An agent inside a banking app is the vertical where the boundary between deciding and executing is the whole design, and a confirm-and-pay flow end to end is the same mechanism written as a build.

Money-moving tools held behind a confirm card, with only the settlement tool escalating to a device biometric.
Animation: five side-effecting tools in a live merchant assistant, every one of them confirm-gated, and exactly one escalating to a device biometric. Drawn from the tenant configuration rather than from a live session.

Three tiers, and why the top one stays small#

Not every write deserves a biometric prompt. A prompt that fires on everything is a prompt people learn to clear without reading, which leaves you the friction and none of the protection.

The tier is a property of the tool, not of the sentence the user happened to say.
TierTypical toolsWhat the user does
ReadBalance, order status, transaction historyNothing
Reversible writeCreate a payment link, issue an invoice, move a deliveryOne tap on the arguments
IrreversibleSettle funds, transfer, cancel with a penaltyDevice biometrics

In our own merchant assistant exactly one tool sits in the bottom row. Everything else that writes is a tap. That ratio is deliberate: the rarer the biometric prompt, the more attention it gets when it appears.

The platforms document this tiering for you. Android’s BiometricPrompt exposes BIOMETRIC_STRONG (Class 3), BIOMETRIC_WEAK (Class 2) and DEVICE_CREDENTIAL, combinable with a bitwise or, and supports auth-per-use keys that require authentication each time the app touches guarded data, which Google says are “particularly useful for high-value transactions” such as “making a large payment” or “updating a person’s health records” (Android developers). That is the same three-rung ladder, written by a platform vendor. Which of your own actions earn the top rung is worth deciding deliberately rather than by default.

There is an accessibility argument here that almost nobody makes. WCAG 2.2 success criterion 3.3.8 says a cognitive function test, which includes “memorization, such as remembering a username, password, set of characters, images, or patterns”, must not be required in an authentication step unless an exception applies, and the understanding document is explicit that authentication methods provided by the operating system, naming Touch ID and Face ID, are not cognitive function tests (W3C). A biometric confirm removes a memory test. It is an accessibility improvement that happens to also be a security control.

What goes on the card#

The card renders the arguments the runtime is holding. Not a paraphrase of them, and not a fresh summary the model wrote. The values that will be transmitted, formatted for a human.

Four things have to be on it.

The amount and the currency, in full. Rounded display values are how an order-of-magnitude error survives review.

Who or what it affects, named the way the user names it rather than by internal identifier.

Anything that will be deducted, including fees, charges and a changed delivery window. If the user learns it after approving, the card failed.

A cancel that is as easy to hit as the approve. A confirm step with a hard-to-find escape is a dark pattern wearing a safety costume.

The confirm card cropped to the widget area, showing the single total the held call will send if the user approves it.
This tool takes one argument, so the card carries one line. A card with five arguments carries five, and none of them are summarised.
The same payment-link flow conducted entirely in Modern Standard Arabic with a right-to-left layout, ending in a shareable link, with the confirm card in the middle of the flow.
The same held call, the same card, a right-to-left layout. The arguments are the arguments in any language, which is the advantage of rendering them rather than re-narrating them.

And what the agent says while it is on screen#

The spoken answer describes what is about to happen and stops. It does not say “confirm with Face ID”, does not say “approve below”, and does not name the method at all. Two reasons. The method differs by device and by tier, so the sentence is often wrong. And an agent that reliably announces its authentication step has written the script for anyone impersonating it.

There is a third reason, and it is the strongest: the agent is not told which gate it got. The tier decision happens in the runtime after the model has finished producing its turn, so the model has no variable to reason about and nothing to negotiate over. It cannot talk its way past a gate it cannot see, and it cannot accidentally promise the user a cheaper one.

What happens after the tap#

Approval is its own request. The app sends the approval alongside the held call’s identity, the runtime revalidates that the call is still the one it recorded, and only then does it execute. Re-deriving the arguments from the conversation at this point would defeat the whole mechanism, so the runtime uses what it held and nothing else.

Two properties are worth building in from the start, because retrofitting either is unpleasant.

The approval is device-bound. A biometric check on a phone succeeds against the device, not against your server, so on its own it tells your backend nothing. Binding the session to a key held in the device’s secure element is what turns a local success into something the backend can trust, and it is why Android’s CryptoObject exists: it ties a successful authentication to a cryptographic operation rather than to a boolean your app reports.

Every held call is logged with a correlation id, including the ones never approved. The abandoned confirms tell you more about a misbehaving agent than the completed ones do.

Declines, timeouts, and the user who walks away#

Most of the design work is in the three outcomes that are not approval.

Four of the five branches end without executing anything. That ratio is what makes the mechanism worth having.

A decline is a product event, not an error. The agent should acknowledge it in one sentence, keep the context, and offer the thing the user is likely to want next. Asking “are you sure?” a second time is how you teach people to stop reading cards.

An abandonment leaves a held call with nobody attached to it. Reap those on a timer and log them; a sudden rise in abandoned confirms usually means the card is showing something surprising, which is information about your fee disclosure rather than about your agent.

An expiry is the case people forget. The held call was built from numbers that were true when it was built. If the user comes back six minutes later, the balance may have moved. Expire held calls, say plainly on the card that it expired, and rebuild from a fresh read rather than executing against figures nobody has seen. A refusal is a better outcome than a surprise.

Every one of these branches is worth a test that runs on every deploy, which is the argument for treating reliability as something you measure rather than something you assert.

Common questions#

Should an AI agent ask permission before taking an action? Before any action that changes state, yes, and the requirement belongs in server code rather than in the prompt. A prompted confirmation can be bypassed by an insistent user, a long context, or text the agent read from a third party. A code-level branch cannot.

What is a confirm card? A widget rendering the exact arguments of a tool call the runtime is holding. The user approves or declines it, and only an approval releases the call. The model proposes the call and does not execute it.

Which agent actions need biometrics? The irreversible ones. Reads need nothing, reversible writes need one tap on a card showing the arguments, and actions that move money or cannot be undone earn a device biometric. Keeping the top tier small is what keeps the prompt meaningful.

What happens if the user never answers? The held call expires and nothing runs. Rebuild from a fresh read rather than executing against stale figures, and log the abandonment, because a rise in abandoned confirms usually means the card is disclosing something users did not expect.

Does a biometric check prove anything to my backend? Not by itself. It succeeds against the device. Bind the session to a key in the device’s secure element so that a successful check produces something cryptographic your backend can verify, rather than a boolean the client reports.

Sources#

Filed underSecurityConfirmationBiometrics

Next

How an action is gated, what the confirm step guarantees, and what leaves the device.

Read how the write path is secured

The rest of Safety

Open the cluster

Confirm-first actions, biometric tiering, scoped tokens and the audit trail you need before an agent touches money.

Elsewhere on the map