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 nocontrol, 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 ahardaction itself — only a human click resolves it. For destructive, irreversible, or costly actions.
// 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.

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.
<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.