Agent Action Approve

A review screen for an AI agent's tool call that spells out what it will delete, send or change and how far it reaches, before someone approves it.

StopsUnreviewed agent tool calls

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/agent-action-approve.json

The files land in your project. They're yours to edit.

Usage

agent-action-approve-usage.tsx
import { AgentActionApprove, type AgentDecision, type ToolCall } from "@/components/ui/agent-action-approve"

<AgentActionApprove
  key={index}
  call={CASES[index].call}
  allowedHosts={["api.github.com"]}
  onDecision={async (d: AgentDecision) => {
    await new Promise((r) => setTimeout(r, 400))
    setLog(d.type === "always" ? `Rule saved: ${d.rule}` : d.type === "once" ? "The agent ran it once." : "The agent was told no.")
  }}
/>

What it catches

  • Tokens that widen the reach: a lone \ (drive root), an unset $VAR, ../, 8.3 short names
  • A stated intent that disagrees with what the call actually does
  • MCP tools labelled read-only whose call writes or deletes
  • Commands hidden inside bash -c, cmd /c or encoded PowerShell, and curl | bash
  • "Always allow" rules that would also let through other commands, and refuses them for npx, bash -c, chained or irreversible calls

Props

AgentActionApprove props
PropTypeDefault
call* (required)

The tool call the agent wants to make. Null or junk renders an empty, safe card.

ToolCall | null | undefined—
allowedHosts

Hosts an upload may go to without a warning, e.g. ["api.github.com"].

string[]—
holdMs

How long a dangerous call must be held, in ms. Default 1200.

number1200
onDecision

Called with the decision. May return a Promise: buttons show a pending state and can't fire twice.

(decision: AgentDecision) => void | Promise<void>—
messages

Override any visible string.

Partial<Omit<Messages, "risk" | "scope" | "undoLabel" | "refused">> & { risk?: Partial<Messages["risk"]> scope?: Partial<Messages["scope"…—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Decision result receives focus (tabIndex=-1) and status text is announced in a role="status" region
  • Hold-to-approve works with Space or Enter held down, not just the pointer
  • Refusals of an "always" rule use role="alert"
  • Risk level is shown with text and icon, not colour alone; motion respects prefers-reduced-motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Enforce the decision at the tool boundary: the agent runtime must refuse calls that were not approved, and check the caller's authorisation for the target.

  • A review UI only: whether a call is actually blocked depends on your host app honouring the decision
  • Static analysis of the call text; it cannot know what a script or binary does once run
  • Unknown tools and argument shapes are described less precisely than shell and SQL calls
  • "User approved this" does not mean the operation is safe

What it stops

Unreviewed agent tool calls

Also known as

tool approval · human in the loop · mcp · coding agent · claude code · cursor · tool call confirmation · rm -rf

Field type

Dependencies

Pairs well with