---
title: "Confirm risky actions with a token bound · Agent Wranglers"
url: https://agentwranglers.se/playbook/agents-as-users/safe-actions/confirmation-token
id: AGT-21
level: Required
risk: money
updated: 2026-10-22
tokens: 1408
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Confirm risky actions with a token bound to the exact request

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: AGT-21
- Level: Required
- Chapter: [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md)
- Rule: An agent call that pays, deletes, sends to many people or cannot be undone first returns a confirmation token. Only a second call with that token, the same arguments and the person's yes carries it out.
- Check: A test calls `refund_order` without a token and gets a preview, not a refund. A second call with the token and a changed amount is refused. The same token used twice refunds once. A token older than 10 minutes is refused.
- Risk: Money lost or taken twice
- How: The client's own approval step is outside your control, so enforce the pause on the server. Return a preview and a token tied to the user, the tool and a hash of the arguments. Let it expire after 10 minutes. Get the person's yes through an `input_required` round, as [Sign the state you hand back for a second round](https://agentwranglers.se/playbook/agents-as-users/mcp-servers/signed-request-state.md) says, or through your own app. Refuse a token used twice, used by someone else or used with changed arguments. Set the money and recipient thresholds in one place, and keep them below the caps in [Cap what each agent grant may spend](https://agentwranglers.se/playbook/agents-as-users/quotas-and-pricing/spend-cap-per-grant.md). The client side of the same idea is [Ask a person before the agent spends or deletes](https://agentwranglers.se/playbook/adversarial-testing/ai-agents-and-models/human-approval.md).

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
import { createHash, randomUUID } from 'node:crypto';
const CONFIRM_TTL_MS = 600_000;
const argsHash = args => createHash('sha256').update(JSON.stringify(args, Object.keys(args).sort())).digest('hex');

export async function confirmOrPreview(tool, args, user, preview, run) {
  const { confirmation, ...rest } = args;
  if (!confirmation) {
    const id = randomUUID();
    await confirmations.insert({ id, userId: user.id, tool, hash: argsHash(rest), expiresAt: Date.now() + CONFIRM_TTL_MS });
    return { needsConfirmation: true, confirmation: id, preview: await preview(rest) };
  }
  const used = await confirmations.consume(confirmation, { userId: user.id, tool, hash: argsHash(rest), now: Date.now() });
  if (!used) return { isError: true, content: [{ type: 'text', text: 'Confirmation is missing, expired or for other arguments. Ask again.' }] };
  return run(rest);
}
```

### Python example

```python
import hashlib, json, time, uuid

CONFIRM_TTL_SECONDS = 600

def args_hash(args: dict) -> str:
    return hashlib.sha256(json.dumps(args, sort_keys=True).encode()).hexdigest()

def confirm_or_preview(tool: str, args: dict, user, preview, run):
    rest = {key: value for key, value in args.items() if key != "confirmation"}
    if "confirmation" not in args:
        token = str(uuid.uuid4())
        confirmations.insert(id=token, user_id=user.id, tool=tool, hash=args_hash(rest), expires_at=time.time() + CONFIRM_TTL_SECONDS)
        return {"needsConfirmation": True, "confirmation": token, "preview": preview(rest)}
    if not confirmations.consume(args["confirmation"], user_id=user.id, tool=tool, hash=args_hash(rest), now=time.time()):
        return {"isError": True, "content": [{"type": "text", "text": "Confirmation is missing, expired or for other arguments. Ask again."}]}
    return run(rest)
```

## Sources

- [OWASP Top 10 for LLM Applications, LLM06 Excessive Agency](https://genai.owasp.org/llmrisk/llm062025-excessive-agency/)
- [MCP specification on tools (user interaction model)](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)

## Related rules

- AGT-18 [Sign the state you hand back for a second round](https://agentwranglers.se/playbook/agents-as-users/mcp-servers/signed-request-state.md) in Agents as users
- AGT-25 [Cap what each agent grant may spend](https://agentwranglers.se/playbook/agents-as-users/quotas-and-pricing/spend-cap-per-grant.md) in Agents as users
- SEC-82 [Ask a person before the agent spends or deletes](https://agentwranglers.se/playbook/adversarial-testing/ai-agents-and-models/human-approval.md) in Adversarial testing
- AGT-22 [Return an undo handle with every agent change](https://agentwranglers.se/playbook/agents-as-users/safe-actions/undo-handle.md) in Agents as users

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/agents-as-users/safe-actions/confirmation-token">Agent Wranglers agents as users playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Offer a dry run for every change](https://agentwranglers.se/playbook/agents-as-users/safe-actions/dry-run.md)
- Next: [Return an undo handle with every agent change](https://agentwranglers.se/playbook/agents-as-users/safe-actions/undo-handle.md)
- [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md)
