Approval gates
Three related mechanisms, from “the agent asks nicely” to “the human imposes it”:
1. pidge approval — the JSON go/no-go
Section titled “1. pidge approval — the JSON go/no-go”pidge approval --title "Deploy to production?"# → blocks; chosen_action.action_id is "grant" (approved, behind Face ID) or "deny"A recipe over important: Approve (Face ID) / Reject buttons + --wait. Use it when your code
wants the decision as JSON to parse. Pass your own --actions to override the pair.
One more outcome to handle: action_id can come back "done" — a neutral, terminal Done
the server appends to every ask (v93), meaning the human cleared the pendency without
deciding. Treat it as not approved (it’s not a grant), stop waiting, and don’t retry the
same ask.
2. pidge approve — the exit-code gate for hooks
Section titled “2. pidge approve — the exit-code gate for hooks”pidge approve "Run the schema migration?" --body "Drops legacy_orders" --timeout 300Built for permission hooks that must fail closed. The answer is the exit code:
| Exit | Meaning |
|---|---|
0 |
explicit allow (Face ID tap) — the ONLY success |
1 |
deny, timeout, no answer, broken channel, or an HTTP failure on the send |
2 |
only a raw network error — the send never reached the server |
Non-zero always means “not approved.” It never exits 3/4 like the other waiting
commands — ambiguity is resolved toward deny.
Claude Code PreToolUse hook
Section titled “Claude Code PreToolUse hook”#!/usr/bin/env bashinput=$(cat) # the hook JSON on stdintool=$(printf %s "$input" | jq -r .tool_name)cmd=$(printf %s "$input" | jq -r ".tool_input.command // (.tool_input|tostring)")if pidge approve "Allow $tool?" --body "$cmd" --timeout 300 >/dev/null 2>&1; then exit 0 # human approved (Face ID) → let the tool runelse echo "Blocked: no human approval for $tool" >&2 exit 2 # exit 2 = PreToolUse BLOCK; fail-closed on deny/timeout/errorfi(pidge approve --help carries this example too.)
3. The human-imposed knob — “Require approval · Face ID”
Section titled “3. The human-imposed knob — “Require approval · Face ID””In the app, the human can turn on Require approval · Face ID for any profile (off by
default everywhere). When it’s on for, say, important, every ordinary send on that profile
becomes an Approve-with-Face-ID decision — even a plain send with no buttons. The server
injects a single approve action; the human’s tap reaches the agent as
chosen_action.action_id: "approve" via poll, webhook or listen --all.
The agent didn’t ask for this — the human imposed it. If your sends start reading back
actions: ["approve"], requires_action: true, that’s what happened: read the echo, don’t
fight it.
Gated actions in general
Section titled “Gated actions in general”Any custom action marked destructive, confirm or biometric — and every custom action —
renders only on the tap-through detail screen behind a confirm/Face ID gate, never as a
lock-screen banner button. A stray banner tap can’t fire a risky action. See
Sending → Buttons.
