Sessions

Tool Approvals

Query, display, and resolve unrestricted tool requests within a specific Session

Tool Approvals

An approval belongs to the Session that started the tool call, not to the top-level Agent. This prevents a decision from being submitted to the wrong conversation when Sessions run in parallel.

When a tool such as Shell requests extra permission, its Tool Part in the current Assistant Message enters approval-required.

ready -> approval-required -> running -> completed
                         \-> failed

Display a live request and resolve it

An approval-required Tool Part carries the complete approval snapshot. The subscription only reports state; it does not expose a command channel:

const unsubscribe = session.subscribe((mutation) => {
  if (
    mutation.variant !== "part" ||
    mutation.type !== "tool" ||
    mutation.part.state !== "approval-required" ||
    !mutation.part.approval
  ) {
    return;
  }

  show_approval_request(mutation.part.approval);
});

// After user confirmation:
await session.resolve_approval({
  approval_id,
  decision: "approved",
});

The only decisions are approved and denied. Keep subscribe() as a data-only stream and call resolve_approval() only in response to an explicit user action. The next Tool Part Mutation is the authoritative resulting state.

Query first, resolve later

An approval screen, a reconnected client, or a client without a continuous subscription should use Session methods:

const pending = await session.approvals();

for (const approval of pending) {
  render_approval({
    tool_name: approval.tool_name,
    command: approval.command,
    cwd: approval.cwd,
    reason: approval.reason,
    operation: approval.operation,
  });
}

await session.resolve_approval({
  approval_id: pending[0].approval_id,
  decision: "denied",
});

resolve_approval() only handles a pending request in the current Session. When the request is missing, already resolved by another client, or belongs to another Session, its result has success: false; it is never resolved across a Session boundary.

Approval modes

const current = await session.approval_mode();

await session.set_approval_mode({
  mode: "always-allow",
});
ModeBehavior
askDefault. Every unrestricted request enters the pending queue.
always-allowNew future requests in this Session are automatically approved.

always-allow does not change the sandbox type, affect other Sessions, or resolve requests that are already pending. A product should surface the active mode clearly and place elevated permission behind explicit user choice.

UI implementation details

  • Use approval_id as the stable approval-card key and tool_call_id to link it to the Tool Part.
  • Keep a tool output area waiting while the Part is approval-required; do not mark it complete.
  • After submitting a decision, wait for the next Part Mutation to show running, completed, or failed; do not synthesize success client-side.
  • One request can have multiple subscribers; resolve_approval()'s success is authoritative.
  • On Session change, clear the old approval queue. Never send an old Session's approval_id to a new Session.

See Live subscriptions for complete Tool Part state and rendering.