Human-in-the-Loop approval workflow
Supported modes: Hosted Hybrid Available in: Teams (Free + Solo can read but cannot enable; needs a separate approver) SDK: 1.6.0+ (validator-additive in 1.5.8)
When the policy engine denies a tool call, the user's only options today are: read the deny message, edit the policy, redeploy. Friction is high. Customers soften their policies to avoid the friction, and governance erodes. Approvals turn the deny moment into a request moment, optionally, when a per-scope toggle is on.
How it works
- Author a strict policy. Tag specific rules with
escalate_on_deny: true. - Agent hits the rule. The SDK sees the deny is approval-eligible and (with
client.request_approval(...)) POSTs an approval request to the backend. - Approver gets notified. In-app bell + email (with a magic-link for cold sessions).
- Approver picks one: Deny / Approve once / Approve for 24h, 7d, 30d / Approve forever.
- SDK resumes. Agent proceeds with the call (allow path), or honors the deny (deny path).
Every approval is auditable: who approved, when, why, what grant was created or what policy diff was applied.
Decision kinds
The approver picks one of:
| Decision kind | What it does | Storage |
|---|---|---|
approved_once | Single call only; auto-revoke after first use OR 5 minutes | hitl_grants row, args_hash-bound |
approved_timed | 24h, 7d, 30d, or custom (max 90d) | hitl_grants row, expires_at-bound |
approved_forever_grant | Forever; revocable from /grants admin page | hitl_grants row, expires_at IS NULL |
approved_forever | Policy edit: inserts allow rule above the deny | Policy version bump; rule carries created_by_hitl metadata |
The default is approved_once. Admins ratchet up scope when they see a pattern.
"Who can use this approval?" picker
For every grant decision, the approver picks the principal scope:
- Just <requestor_email> (default for tools + timed grants)
- Anyone using project X
- Anyone on machine Y
- Anyone using key Z
- Custom condition (admin edits the rule manually)
For secrets, the default is user-scoped even on approved_forever. A long-lived secret grant for the whole project undermines the vault. See Secrets approvals.
Identity layer
The approval flow needs to know which human triggered the request. API keys are machine credentials; they can be shared. The SDK requires controlzero install --email <email> at install time; the email is sent on every backend call as the X-CZ-Requestor-Email header.
This matters most for shared API keys (project keys, CI keys). See Multi-user keys for the threat model and forensic story.
What approvals are NOT
- Not a replacement for good policy authoring. Approvals are the escape hatch when a deny would otherwise force a customer to soften the rule.
- Not silent. Every approval is in the audit log. The
/approvalspage is the canonical surface for security review. - Not unlimited. Custom grant duration is capped at 365 days (default cap 90 days, configurable per scope).
- Not for Free / Solo tiers. Both tiers have a single user; self-approval theater gives no governance. Upgrade to Teams to use it.
See also
- Secrets approvals. Approvals on credential reads
- Multi-user keys. Identity for shared API keys
- SDK:
request_approval+wait. The SDK API - E1301 approval timeout
- E1307 identity required