Files
claude-agent-acp/docs/permission-extension.md
hwisu bbae5c2c09 fix: show shell commands in permission prompts (#1070)
## Summary
- use the canonical tool-call title for permission prompts, so shell
approvals show the exact command instead of the model-authored
description
- map `PowerShell` through the same shell presentation path as `Bash`
- complete the PowerShell terminal lifecycle with matching
`terminal_info`, `terminal_output`, and `terminal_exit` metadata,
including failed commands
- add focused regression coverage without changing the existing Bash
lifecycle tests

Fixes #1068

## PowerShell input and result contract
`PowerShell` is a distinct tool name, but its documented input fields
match `Bash`: the executable command is in `command`, while
`description` is an optional human-readable summary. The Claude Code
hooks reference documents the shared `command`, `description`,
`timeout`, and `run_in_background` fields, and the tools reference
likewise states that PowerShell hooks receive the command in
`tool_input.command`.

Anthropic closed
[claude-code#83647](https://github.com/anthropics/claude-code/issues/83647#issuecomment-5310033500)
after documenting this equivalence. Reusing the SDK's exported
`BashInput` shape for the common input fields is therefore intentional;
the SDK currently does not export a separate `PowerShellInput` type.

PowerShell results do not need a PowerShell-specific output assumption
here. The result mapper first shape-checks structured stdout/stderr and
otherwise falls back to the standard raw `tool_result` string or
content-block array. Routing PowerShell through that existing mapper
preserves this fallback while completing the adapter's capability-gated
terminal sequence:

1. initial `tool_call` with `terminal_info`
2. `tool_call_update` with `terminal_output`
3. final `tool_call_update` with `terminal_exit`

All three events use the original tool-use ID, so the client can
reconcile and close the terminal. Failed PowerShell results also bypass
the generic error renderer and finish the terminal with a non-zero
display exit code.

The permission presentation introduced in #1004 already handled `Bash`
and `PowerShell` together when choosing a description-based permission
title, but the canonical tool presentation and terminal result paths
only handled `Bash`. This PR completes both paths.

References:
- [Claude Code hooks: PowerShell
input](https://code.claude.com/docs/en/hooks#powershell)
- [Claude Code tools reference: PowerShell
tool](https://code.claude.com/docs/en/tools-reference#powershell-tool)
- [Anthropic schema
confirmation](https://github.com/anthropics/claude-code/issues/83647#issuecomment-5310033500)

## Verification
- `npm run check`
- `npm run build`
- `npm run test:run -- src/tests/tools.test.ts -t "PowerShell terminal
output"` (1 passed)
- full suite with an empty temporary Claude config:
`CLAUDE_CONFIG_DIR=<empty temporary directory> npm run test:run` (1070
passed, 27 skipped)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-09-17 13:34:05 +00:00

15 KiB

Permission extension

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."
    }
  }
}
Field Required Type Meaning
version yes integer 1 Permission presentation schema version.
title yes non-empty string The standard tool-call operation title.
description no string Temporary diagnostic SDK decision reason.
defaultToNo no true The 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 id Meaning
allow-once Allow this call without changing permission state.
allow-with-updates Allow and apply the displayed durable effect.
allow-skill-exact Allow the exact Skill invocation in local settings.
allow-skill-prefix Allow the parameterized Skill prefix in local settings.
exit-plan-auto Exit plan mode and set session mode to auto.
exit-plan-bypass Exit plan mode and set session mode to bypassPermissions.
exit-plan-accept-edits Exit plan mode and set session mode to acceptEdits.
exit-plan-default Exit plan mode and set session mode to default.
exit-plan-clear-auto Clear context, continue the plan, and use auto.
exit-plan-clear-bypass Clear context, continue the plan, and bypass permissions.
exit-plan-clear-accept-edits Clear context, continue the plan, and accept edits.
reject Deny 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 family Permission choices and effects
Bash / PowerShell One-time allow; exact representable SDK bundle with its complete effect in the option label; reject.
Read / Glob / Grep One-time allow; matching session-scoped read grant; reject.
Edit / Write / NotebookEdit One-time allow; matching session edit grant, including .claude handling; reject.
WebFetch One-time allow; generated domain:<hostname> local rule; reject.
Skill One-time allow; exact skill and optional prefix:* local rules; reject.
EnterPlanMode Enter plan mode once, or reject and continue implementing.
ExitPlanMode One keep-context and one clear-context elevated mode (auto > bypass > accept edits), manual approval, or reject.
SandboxNetworkAccess One-time allow; exact SDK host rule when representable; reject.
Computer Use MCP One-time allow; exact representable SDK allow suggestion; reject.
MCP One-time allow; exact representable SDK allow suggestion for that tool; reject.
WebSearch / Agent / Task One-time allow; generated whole-tool local rule; reject.
ReviewArtifact / Workflow Generic fallback behavior until SDK renderer state is available.
Monitor / unknown tools Generic 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