Permission prompt

Free · MIT

A tool call waiting for a person's go-ahead: what it will do, why it reached them, and the permission mode the run is in. They can edit the input before allowing it and see exactly what they changed; Allow then sends that input, nothing else. "Don't ask again" rules are offered as boxes nobody has ticked, and are kept only if the person ticks them. When the agent is asking questions instead, it shows them as a short form. Words from the request (its title, reason, input) are shown as text, never markup.

It sits in the thread, with no backdrop and nothing that dismisses it: the only ways out are Allow and Deny, and nothing else is ever counted as a denial. While the decision waits, its shield pulses slowly; a press on inert space outside it (the gesture that would dismiss a dialog) nudges it instead, so the person sees it is still waiting. Both stop once the answer is being sent.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/permission-prompt

Brings in: button, locale, payload, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { PermissionPrompt, type PermissionRequest } from "@/components/permission-prompt"

const command: PermissionRequest = {
  id: "p1",
  tool: "Bash",
  title: "The agent wants to run npm test -- invoices",
  reason: "No rule allows Bash commands yet.",
  input: { command: "npm test -- invoices", timeout: 120000 },
}

const question: PermissionRequest = {
  id: "p2",
  tool: "AskUserQuestion",
  input: {},
  questions: [
    {
      id: "Which test runner should the new tests use?",
      prompt: "Which test runner should the new tests use?",
      tag: "Runner",
      multiple: false,
      other: true,
      options: [
        { id: "Vitest", label: "Vitest", detail: "Already used by the web app" },
        { id: "Jest", label: "Jest", detail: "Used by the older packages" },
      ],
    },
  ],
}

export default function Example() {
  const [said, setSaid] = React.useState<string>()
  return (
    <div className="flex max-w-lg flex-col gap-6">
      <PermissionPrompt
        request={command}
        mode="default"
        editable
        suggestions={[{ action: "allow", what: "Bash(npm test:*)", scope: "local" }]}
        onAllow={(a) => setSaid(`Allowed${a.updatedInput ? " as edited" : ""}${a.alwaysAllow?.length ? ", and won’t ask again" : ""}.`)}
        onDeny={() => setSaid("Denied.")}
      />
      <PermissionPrompt request={question} onAllow={(a) => setSaid(`Answered: ${a.answers?.map((x) => [...(x.choice ?? []), x.text].filter(Boolean).join(", ")).join("; ")}`)} onDeny={() => setSaid("Not answered.")} />
      <p role="status" className="text-sm text-muted-foreground">{said}</p>
    </div>
  )
}

<PermissionPrompt>

PropTypeDefaultDescription
requestrequiredPermissionRequest—The waiting request.
modestring | undefined—The run's permission mode, as the SDK names it ("default", "acceptEdits", "plan", "auto"…). Left out, it isn't shown.
suggestionsPermissionSuggestion[] | undefined—"Don't ask again" rules to offer. None is ticked.
editableboolean | undefinedfalseLet the person edit the input as JSON before allowing it.
onAllowrequired(answer: PermissionAnswer) => void—Called with the edited input, the ticked rules or the answers.
onDenyrequired() => void—Called when the person denies it (or won't answer the questions).
busyboolean | undefinedfalseThe answer is being sent: both buttons wait, and the waiting pulse stops.
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under permissionPrompt in <LocaleProvider strings={{ … }}>; the table is exported as permissionPromptStrings.

KeyEnglish
labelPermission request: {title}
fallbackTitleThe agent wants to use {tool}
whyWhy you’re asked
modePermission mode
modeDefaultAsk before acting
modeAcceptEditsAccept file edits
modePlanPlan only
modeAutoAutomatic, by a classifier
modeDontAskDon’t ask; deny what isn’t allowed
modeBypassPermissionsAllow everything
blockedPathPath outside what’s allowed: {path}
subagentA subagent asked.
serverFrom the tool server “{name}”, configured in {source}.
inputWhat it will run with
editEdit before allowing
editLabelInput, as JSON
editInvalidThat isn’t a JSON object, so it can’t be used.
changedYou changed
unchangedNothing changed yet.
cancelEditUse the original
rememberDon’t ask again for
rememberHintOptional. Nothing is remembered unless you tick it.
ruleAllowAllow {what}
ruleDenyAlways deny {what}
ruleAskAlways ask for {what}
ruleRemoveStop applying the rule for {what}
setModeSwitch to: {mode}
warnBypassPermissionsAfter this, nothing is checked: the agent can run any command and change or delete any file it can reach.
warnAutoAfter this, a classifier allows or blocks each action instead of you.
directoriesAccess to {what}
otherA permission change: {what}
scopeSessionfor this session
scopeLocalin this project, on this computer
scopeProjectin this project, for everyone on it
scopeUserfor you, in every project
allowAllow
allowEditedAllow as edited
denyDeny
answerSend answers
otherOptionOther
otherLabelYour own answer to: {question}
questionsThe agent has questions

Types

PermissionSuggestion
One "don't ask again" choice, in the plain shape PermissionPrompt shows. claudeSuggestions builds them from the SDK's.
{ /** What the rule does. */ action: "allow" | "deny" | "ask" | "remove" | "mode" | "directories" | "other" /** What it covers, as the source writes it: `Bash(npm test:*)`, a mode's name, directories. Shown as code. */ what: string /** Where it's kept: "session", "local" (this project, this computer), "project" (shared), "user", or the source's own word. */ scope?: string | undefined }
PermissionQuestion
A question the agent asks through the prompt: pick one or more labels, or answer in its own words.
{ id: string prompt: string tag?: string | undefined options: { id: string; label: string; detail?: string | undefined }[] multiple: boolean other?: boolean | undefined }
PermissionRequest
A tool call waiting on a person, in the shape the adapters give (ClaudeApproval fits it).
{ id: string tool: string input: unknown /** The source's own sentence for the request: "Claude wants to run npm test". */ title?: string | undefined description?: string | undefined /** Why it reached a person (no rule allowed it, a safety check, a path outside the project). */ reason?: string | undefined blockedPath?: string | undefined /** Put the focus on Deny: the request grants more than it
PermissionAnswer
{ /** The input as edited, when the person changed it. */ updatedInput?: Record<string, unknown> | undefined /** The suggestions the person ticked, by index. */ alwaysAllow?: number[] | undefined /** Answers to `questions`. */ answers?: { questionId: string; choice?: string[] | undefined; text?: string | undefined }[] | undefined }