WebhookApprovalProvider sends one signed POST to a URL you own for every approval request,
and takes the answer back through a signed POST to the gateway’s /ctrlrun/approvals/ endpoint.
Your side is a small service that turns the request into a Slack message with two buttons and
turns the click into the answer. The grant is written by the same call ctrlrun approve makes;
there is no second approval path.
Prerequisites: pip install ctrlrun (the outbound half is core); pip install "ctrlrun[gateway]" for the inbound endpoint, which the gateway serves; a shared secret; a
URL that can receive the POST.
1
Configure the provider
In a process using the decorator:With the gateway, the same thing is two flags:The secret comes from
$CTRLRUN_WEBHOOK_SECRET or a file, never from a flag value: a
secret on a command line is in every process listing on the host. An http:// URL is
refused unless --allow-insecure-webhook, and then only on loopback.2
Receive the request
One POST per Show the human exactly what is in
APPROVAL_REQUESTED, with the CTRLRun-Signature header, an HMAC-SHA256 over
timestamp.body computed on the exact bytes sent. Verify it before you read the body.action: the name, every argument, who is asking. The
approval will be bound to that hash, so what they see is what will run.3
Send the answer back
POST to
respond_to, signed the same way, with a timestamp within the replay window
(five minutes by default):decision is grant or deny; approver is a non-empty string recorded on the receipt.
A replayed grant inside the window is idempotent, because the record is already granted;
outside it, the timestamp check refuses. The endpoint answers 200 on success and a 4xx with
the reason on a bad signature, an unknown request id, an expired request, or a request
already answered the other way.4
Watch it land
expires_at (the approval TTL, fifteen
minutes by default) and a waiting call raises ApprovalTimeout. Nothing runs.What the provider does not do
It does not build the Slack message or the buttons; that is your service, which knows your workspace. It does not authenticate the approver:approver is recorded as given, which is the
threat model’s stated limit. It does not retry into the future: delivery is two retries with a
short backoff, and an undeliverable request is logged and left pending for ctrlrun approve.
If it didn’t work
signature does not verify: the secret differs, or your side re-serialized the body before signing. Sign the exact bytes.timestamp outside the replay window: clocks differ by more than five minutes.ApprovalTimeout: nobody answered within the TTL; the request is expired, not lost.- The webhook never fires: the provider is not on the
Controlthe protected function uses. With the decorator, passcontrol=control.
Next
- Approval binding: what the answer authorizes.
- Put the gateway in front of MCP.
- Get started · Why.