Permission extension

September 17, 2026 ยท View on GitHub

This document defines the experimental permission presentation extension implemented by claude-agent-acp. The adapter uses standard ACP permission requests and responses, while _meta adds compact request presentation text.

The extension does not replace ACP permissions. RequestPermissionRequest.toolCall, options, RequestPermissionResponse.outcome, and each option's standard kind remain authoritative. A client that does not understand the extension can ignore _meta and render the ordinary ACP request.

Request lifecycle

When the Claude Agent SDK invokes canUseTool, the adapter:

  1. stops immediately if the tool-call signal is already aborted;
  2. ensures the referenced ACP tool_call has been emitted;
  3. validates and snapshots any SDK suggestions: PermissionUpdate[];
  4. builds a tool-specific presentation and ordered option list;
  5. sends session/request_permission with the tool-call cancellation signal;
  6. validates that the selected option was offered in the exact request;
  7. maps the selection to a Claude SDK PermissionResult.

The eager tool_call announcement always precedes the permission request. If the announcement fails, its de-duplication marker is removed so the normal streamed tool-use path may retry it.

Request-level presentation metadata requires no capability negotiation. It is an additive _meta record and may be ignored by clients that do not render it.

Request presentation

The request-level record is placed under RequestPermissionRequest._meta.permission:

{
  "sessionId": "session-1",
  "toolCall": {
    "toolCallId": "toolu_1",
    "name": "Bash",
    "status": "pending",
    "rawInput": { "command": "npm test" }
  },
  "options": [
    { "optionId": "allow-once", "name": "Yes", "kind": "allow_once" },
    { "optionId": "reject", "name": "No", "kind": "reject_once" }
  ],
  "_meta": {
    "permission": {
      "version": 1,
      "title": "npm test",
      "description": "Reason: Needed to verify the change."
    }
  }
}
FieldRequiredTypeMeaning
versionyesinteger 1Permission presentation schema version.
titleyesnon-empty stringThe standard tool-call operation title.
descriptionnostringTemporary diagnostic SDK decision reason.
defaultToNonotrueThe ask must not be approvable by a stray keystroke; the reject options are listed first and a client that pre-selects an option should focus the decline.

The permission title normally duplicates toolCall.title: one operation has one heading across the tool card and approval UI. ExitPlanMode is the deliberate exception and uses the action-oriented permission heading Ready to code?. Commands, paths, URLs, and other structured details remain in rawInput, content, and locations.

description temporarily carries the SDK's non-blank decisionReason for diagnostics, prefixed with Reason: . The SDK description operation subtitle is not copied there.

Tool-call presentation

The permission request carries the same standard ACP tool information used for normal tool updates:

  • name is the SDK tool name (for example Read or mcp__server__tool), matching the standard name field on the initial tool_call update;
  • kind, title, content, and locations come from toolInfoFromToolUse;
  • status is pending;
  • rawInput is the original SDK input object;
  • blockedPath is appended to locations when it is valid and not already present;
  • subagent calls include _meta.claudeCode.parentToolUseId on the tool call;
  • Sandbox Network and Computer Use requests synthesize JSON content when the normal renderer has no content.

Compact text removes control characters, collapses whitespace where appropriate, and enforces length limits. Invalid optional presentation text is omitted instead of being truncated into misleading UI. Shell permission titles (Bash and PowerShell) preserve the full command verbatim, including whitespace and line breaks, without applying compact-text normalization or length limits.

Permission options are fixed. When a durable suggestion contains a command prefix, path, host, or other rule, the adapter includes that value directly in PermissionOption.name, for example Yes, and don't ask again for npm test commands. Selecting the option applies the exact snapshotted Claude SDK PermissionUpdate; the client cannot edit it. A selected reject is distinct from { "outcome": "cancelled" }: cancellation aborts the tool use instead of returning a user rejection.

Standard option ids

Option idMeaning
allow-onceAllow this call without changing permission state.
allow-with-updatesAllow and apply the displayed durable effect.
allow-skill-exactAllow the exact Skill invocation in local settings.
allow-skill-prefixAllow the parameterized Skill prefix in local settings.
exit-plan-autoExit plan mode and set session mode to auto.
exit-plan-bypassExit plan mode and set session mode to bypassPermissions.
exit-plan-accept-editsExit plan mode and set session mode to acceptEdits.
exit-plan-defaultExit plan mode and set session mode to default.
exit-plan-clear-autoClear context, continue the plan, and use auto.
exit-plan-clear-bypassClear context, continue the plan, and bypass permissions.
exit-plan-clear-accept-editsClear context, continue the plan, and accept edits.
rejectDeny the call, optionally returning feedback to Claude.

The adapter stably groups options as allow_once, allow_always, then reject kinds. Options of the same kind retain their builder order. Clients must return the selected option id unchanged.

Durable SDK suggestions

Claude may provide suggestions: PermissionUpdate[]. Before a suggestion can appear as a durable choice, the adapter validates the complete array and takes a structured clone. Validation recognizes:

  • addRules, replaceRules, and removeRules;
  • setMode for known Claude permission modes;
  • addDirectories and removeDirectories;
  • known destinations and rule behaviors;
  • bounded array sizes and bounded non-empty strings.

Unknown update types, invalid values, oversized bundles, non-cloneable data, and empty suggestion arrays are omitted. The snapshot prevents the label shown to the user from diverging from the effect returned after a delayed response.

An SDK suggestion is offered only when the tool-specific builder can accurately describe the entire effect. The adapter never labels one part of a mixed bundle while silently applying the rest.

When matchedAskRule is present, all persistent choices are suppressed. A configured ask rule must continue asking; accepting a generated allow rule that cannot override it would be misleading.

Tool-specific behavior

Tool familyPermission choices and effects
Bash / PowerShellOne-time allow; exact representable SDK bundle with its complete effect in the option label; reject.
Read / Glob / GrepOne-time allow; matching session-scoped read grant; reject.
Edit / Write / NotebookEditOne-time allow; matching session edit grant, including .claude handling; reject.
WebFetchOne-time allow; generated domain:<hostname> local rule; reject.
SkillOne-time allow; exact skill and optional prefix:* local rules; reject.
EnterPlanModeEnter plan mode once, or reject and continue implementing.
ExitPlanModeOne keep-context and one clear-context elevated mode (auto > bypass > accept edits), manual approval, or reject.
SandboxNetworkAccessOne-time allow; exact SDK host rule when representable; reject.
Computer Use MCPOne-time allow; exact representable SDK allow suggestion; reject.
MCPOne-time allow; exact representable SDK allow suggestion for that tool; reject.
WebSearch / Agent / TaskOne-time allow; generated whole-tool local rule; reject.
ReviewArtifact / WorkflowGeneric fallback behavior until SDK renderer state is available.
Monitor / unknown toolsGeneric fallback behavior.

Generated WebFetch, Skill, and non-MCP fallback effects use localSettings. Filesystem grants are offered only when the SDK bundle is session-scoped and covers the current path. Shell labels may describe command rules, read paths, additional directories, or a representable combination.

AskUserQuestion is not a permission dialog. With ACP form elicitation support it is routed through ACP elicitation; without that support it is disabled when the Claude session is created.

Permission modes and bypass

The session exposes the Claude modes available for the current model using Claude Code's labels: Manual, Accept edits, Plan, conditional Auto, and conditional Bypass permissions. The internal dontAsk SDK mode is accepted from settings for compatibility but is not advertised as a user-selectable mode.

Hosts can remove Bypass permissions from a session by sending _meta.claudeCode.options.allowDangerouslySkipPermissions: false on session/new (and again on session/load); the mode leaves the catalog and a bypassPermissions settings default clamps to default.

The ACP mode ids are the Claude SDK wire ids: default, acceptEdits, plan, auto, and bypassPermissions. Manual deliberately retains the SDK id default; manual is only a settings input alias. The adapter does not invent parallel ids for presentation labels.

Like Claude Code, ExitPlanMode offers one elevated keep-context choice by priority: auto, then bypassPermissions, then acceptEdits. A non-empty plan also gets exactly one clear-context choice with the same priority. This requires no ACP capability or setting: after selection the adapter interrupts the old private Claude query, hides that internal rejection from the client, creates a new private query under the same public ACP session, and continues the pending turn with Implement the following plan: followed by the complete plan. Mode, model, agent, effort, Fast mode, and accumulated turn usage are preserved.

The SDK cannot reapply allowedPrompts through a public control request, so those renderer hints do not survive this internal handoff. Ultraplan remains unavailable because its required renderer state is not exposed by the SDK.

The adapter does not auto-allow a callback merely because the session advertises bypassPermissions. Claude Code applies bypass before invoking canUseTool. A request that still reaches the callback is bypass-immune, safety-sensitive, interactive, or forced by an explicit ask rule and must remain visible to the user.

Cancellation and failures

The SDK tool-call AbortSignal is forwarded to the ACP client. The adapter also races the client request locally against the same signal, so a client that ignores cancellation cannot leave Claude's tool call waiting forever.

These cases all abort the tool use with Tool use aborted:

  • the signal was already aborted;
  • the signal aborts while the permission request is open;
  • the client returns a cancelled outcome;
  • the client rejects after cancellation.

A normal selected reject returns behavior: "deny", decisionClassification: "user_reject", and the default refusal message.

Client requirements

A client implementing version 1 should:

  • render standard ACP tool-call content and locations as the action subject;
  • treat request _meta.permission.title and description as optional presentation hints;
  • preserve option ids;
  • ignore unknown _meta fields and future feature names;
  • settle the request when its cancellation signal fires.

The client must not infer storage scope, lifetime, or permission effects from kind or button text. The adapter owns the Claude PermissionUpdate and applies it only after validating the response.

Current limits

The SDK does not expose enough renderer state to reproduce every Claude Code dialog. Version 1 does not implement:

  • Bash output-redirection rewriting;
  • classifier-specific prompt state;
  • ExitPlanMode Ultraplan actions;
  • Computer Use macOS/TCC or application-allowlist dialogs;
  • structured permission lifetime or storage metadata for clients.

These omissions do not weaken the one-time choice. When a durable effect cannot be represented honestly, the adapter sends only one-time allow and reject.

Verification

The executable contract lives in:

  • src/tests/permission-presentation.test.ts for capability and request presentation;
  • src/tests/permission-options.test.ts for tool-specific option construction;
  • src/tests/permission-effects.test.ts for response validation and effects;
  • src/tests/acp-agent.test.ts for request ordering, cancellation, bypass, and subagent attribution;
  • src/tests/session-config-options.test.ts for plan-mode integration;
  • src/tests/live-capability-*-audit.test.ts for opt-in live SDK audits.
npm run build
npm run check
npm run test:run