Tool approval
Asks the person before the agent runs a command or tool, with deny, allow once and always allow.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/tool-approval-card.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/tool-approval-card.json
Build an AI agent tool approval flow in React →
Usage & API
Example
import { ToolApprovalCard } from "@/components/ai/tool-approval-card"
<ToolApprovalCard
tool="…"
command="…"
onDecide={() => {}}
/>Props ToolApprovalCard
| Prop | Type & description | Required |
|---|---|---|
tool | string | Required |
command | string | Required |
cwd | string | Optional |
reason | string | Optional |
decision | ApprovalDecision | null | Optional |
onDecide | (decision: ApprovalDecision) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
While decision is null, the card is a group labeled as approval needed for the tool. The title reads Allow, the tool name, and whether it may run this. reason sits under the title. cwd is a truncated monospace path, hidden on a narrow row. command is in a scrolling pre with a dollar prompt. The footer is Allow once, Always allow, and Deny. Passing a decision removes that panel. deny leaves a red dot, the word Denied, and command struck through. once and always leave a green dot, Allowed once or Always allowed, and the command without a strike. The resolved line is a live region. There is no undo button. Clear decision back to null if you need the prompt again.
Choosing a layout
Use an approval when the next action has a side effect the person did not already accept: deleting a lockfile, applying a migration, sending a request that changes data. The command has to be visible as the thing being allowed, not only described. reason is for the consequence, in a sentence. cwd is for where it will run. A plan is a better ask when the decision covers several steps and no single command is the risk. A question is a better ask when the agent needs a preference and nothing is about to execute.
Use the card inside the reply when the decision should remain readable after the person has answered. A denied command stays in the thread with a strike through it. An allowed command stays without the strike, labeled once or always. Use the banner when the composer is on screen and the approval should feel like part of sending, not like another message. The banner's Always checkbox is per prompt. It resets if you mount the banner again. It does not, by itself, persist a policy. You do that when onAllow receives true.
What is the difference between allow once and always allow?
On the card they are different arguments to onDecide: once and always. The resolved line uses a different label for each, and neither one stores a rule inside the component. On the banner there is one Allow button. The Always checkbox decides the boolean passed to onAllow. What always means for later commands is up to the caller that receives true.
How does a denied command stay in the transcript?
Keep rendering the card and pass decision deny. The prompt unmounts and a single line remains: Denied, then the command with a strike. Pass once or always for the allowed versions of that line. Unmounting the card removes the record. The banner cannot do this. It has no decision prop, so the record of a banner decision has to be some other row you render yourself.