Start here

  • Getting started
  • Backends
  • Connect your coding agent
  • How it works

Declaring your app

  • Actions, surfaces & elements
  • Hand mode
  • Control & confirmation

Polish & operate

  • Appearance
  • The Watch panel

Reference

  • Advanced
  • Configuration
  • Browser & framework support
  • Troubleshooting & FAQ
All docs

Docs

Control & confirmation

Gate an action by how much it costs to get wrong: open, a spoken confirm, or a hard on-screen click the agent can't fake.

Every action carries a control level — a per-action setting that decides how firmly a call is gated before it fires. Choose it by one question: how costly is this to get wrong? A read is free to retry; a delete is not. The level is the only knob, and it is declared right on the action, next to its run handler.

The three levels

  • open (default) — fires immediately, no gate. For reads and anything reversible. If you declare no control, this is what you get.
  • soft — the agent must confirm out loud before firing (“Shall I archive this?”) and only proceeds on a yes. For actions that are meaningful but recoverable.
  • hard — the SDK raises an on-screen confirmation card (a summary plus Confirm / Cancel) that a real person has to click. The agent can never fire a hard action itself — only a human click resolves it. For destructive, irreversible, or costly actions.
tsx
// hard — irreversible. Only a human click resolves it; the agent cannot fire it.
useAction({
  name: "deleteProject",
  description: "Permanently delete the current project and everything in it.",
  control: "hard",
  confirmationSummary: () => "Delete this project and all its tasks? This can't be undone.",
  run: () => deleteProject(project.id),
});

// soft — recoverable. The agent confirms out loud, then fires on a yes.
defineAction({
  name: "archiveThread",
  description: "Archive the open conversation. Use when the user asks to archive or file it.",
  control: "soft",
  run: () => archiveThread(),
});

The hard confirmation card

The card is SDK-rendered and SDK-authoritative. The SDK draws it on the user's device from confirmationSummary — a function, so it never crosses the wire; if you omit it, the card falls back to the action's description. Supply it for custom phrasing or a live count (“Delete 12 tasks?”).

The agent only ever sees the resolved outcome — confirmed, cancelled, or timed out. It cannot render the card, pre-fill it, or click it. That is what makes hard a genuine human gate rather than a prompt the model could talk its way past: the decision physically happens in the browser, under the user's finger.

The hard-confirmation card reading 'Delete invoice #1042?' with Cancel and Confirm buttons
A hard action raises this card — only a real click resolves it. The agent can't.

A safety net for destructive names

If you ship an action whose name reads destructive (a delete…, remove…, purge…) and declare no control, the SDK resolves it to soft rather than open and logs a one-line dev nudge telling you it did. It is a backstop for the case you forgot, not a substitute for choosing: set control explicitly — hard for a real click, open to opt out. You can also move the whole-app baseline off open with the defaultControl prop on CoworkkitProvider.

Control vs. Hand mode

control and Hand mode are different axes, and they compose. Hand mode answers may the agent touch the UI at all — the user-armed switch that gates every element action. control answers how firmly is this one call confirmed once it is allowed to run. A hard-controlled element action has to clear both gates: the user must have Hand mode armed, and then click Confirm on the card.

Every call is on the record

Each settled call emits one record — what ran, the level it was gated at, the gate decision (fired, needs_hand_mode, confirmed, cancelled, or timeout), and the outcome. Two places consume it: the onActionRecord Provider callback, for routing into your own logging, and the Watch panel timeline, for reading it live during a build.

tsx
<CoworkkitProvider
  getToken={getToken}
  onActionRecord={(record) => {
    // e.g. "deleteProject" · "hard" · "confirmed"
    audit.log(record.action, record.control, record.gate);
  }}
>

The SDK emits and never stores these records — the audit trail is yours to keep wherever you keep the rest.

PreviousHand modeNextAppearance

Machine-readable: this page as Markdown · all docs (llms.txt)