Human-in-the-Loop Tool Approvals for OpenClaw
When an OpenClaw agent goes to delete a repo, refund a charge, or send a customer email, you want a human to approve it. Here's how MCP gateway approval queues turn 'agent acted on its own authority' into 'agent acted with explicit approval'.
The hardest part of running an OpenClaw agent in production isn't the secrets. It's the actions.
An agent that reads STRIPE_API_KEY is bounded — the worst case is data exfiltration of one secret. An agent that uses STRIPE_API_KEY to call stripe.refund is unbounded — the worst case is everything from "$50,000 in unintended refunds" to "compliance incident."
This post is about the gap between "the agent has the credential" and "the agent should be allowed to use it for this specific action." The mechanism is MCP gateway approval queues: per-tool rules, a pending queue for sensitive calls, and a one-shot grant flow that lets the agent retry exactly once after approval.
The conceptual model
Treat tool calls like changesets. Some are routine — read a Slack channel, list GitHub issues, query a database. Some are dangerous — send an email, refund a charge, delete a repo, run a shell command on production.
For routine calls: log them, rate-limit them, move on.
For dangerous calls: pause, queue them for a human, and only proceed if a human approves. With an audit trail of who approved, when, and why.
The MCP Gateway implements both. You configure which tools are which.
Setting up an approval rule
In the secr dashboard, under MCP Gateway → Rules, create a rule of type Require Approval:
Name: Production write approval
Tools requiring approval:
- github.delete_repo
- stripe.refund
- shell.exec
- email.send_customer
Environments requiring approval:
- production
You can add tool names that aren't in the predefined MCP list — github.delete_repo is an OpenClaw-style tool name, not an MCP secret operation. The gateway accepts arbitrary string tool names up to 100 characters.
When an OpenClaw agent (with @secr/openclaw-plugin installed — the plugin's before_tool_call hook handles the gate) tries to call any of those tools, the gateway:
- Records a
mcp_tool_callsrow withstatus="approval_required" - Returns an error to the agent:
This action requires approval. An admin must approve it before retrying. - The call doesn't execute — no API request goes out
The OpenClaw agent will surface that error to the user (or its operator), who now knows to ask a human admin to approve the action.
What an admin sees
The dashboard's MCP Gateway page has a Pending tab with a count badge whenever there are calls awaiting approval:
[Overview] [Rules] [Pending (3)] [Tool Calls] [Sessions]
Each pending entry shows:
- The tool name (
github.delete_repo) - The agent that requested it (
support-bot/secr_agent_a1b2…) - The parameters (with sensitive keys like
token/password/valueredacted) - The environment, project, source IP, and timestamp
The admin clicks Decide, optionally adds a note ("approved for one-time cleanup of acme/old-staging"), and chooses Approve or Deny.
This writes back to the mcp_tool_calls row:
status: "approved" | "denied"approvedBy: <user_id>approvedAt: <timestamp>decisionNote: "..."
So a year from now, you can run a query like "show me every approval for github.delete_repo and who approved each one." The audit trail is the point.
How the agent retries
This is the part most approval-flow designs get wrong. After the admin approves, how does the agent know?
A few wrong answers:
- Polling — too many requests, and what if the agent gives up before approval lands?
- Webhooks back to the agent — most agents aren't web servers
- Long-lived blocking call — fragile under network failures, no atomicity
The right answer, which is what secr ships, is a one-shot grant with atomic consume:
- After admin approves, the row sits in the database with
status="approved"andconsumed_at: null. - The agent retries the same tool call. The gateway client first asks the server: "do I have an active grant for
github.delete_repo?" - The server runs a single SQL transaction:
UPDATE mcp_tool_calls SET consumed_at = NOW() WHERE id = ( SELECT id FROM mcp_tool_calls WHERE org_id = $1 AND agent_id = $2 AND tool_name = $3 AND status = 'approved' AND consumed_at IS NULL AND approved_at > NOW() - INTERVAL '10 minutes' ORDER BY approved_at DESC LIMIT 1 FOR UPDATE SKIP LOCKED ) RETURNING id - If a row matches, it's marked consumed and the gateway returns
granted: true. The tool call proceeds. - If no row matches (already consumed, expired, wrong tool, wrong agent), the gateway blocks again.
The key properties:
- Exactly-once —
FOR UPDATE SKIP LOCKEDmeans two concurrent agent retries can't both consume the same grant. One wins, one getsgranted: false. - Time-bounded — approvals expire 10 minutes after the admin's decision. If the agent doesn't retry in that window, the approval is gone and a new request is needed.
- Cross-agent isolation — agent B can't consume a grant that was issued to agent A.
- Tool-name strict — a grant for
github.delete_repodoesn't covergithub.archive_repo.
This is the part where small implementation details have outsized security consequences. A non-atomic version of this (read row, then update) would let two concurrent agents both proceed on a single approval. The FOR UPDATE SKIP LOCKED clause is what makes it safe under contention.
What it looks like in code
You don't write any of this yourself. With the plugin installed, the before_tool_call hook runs ahead of every tool invocation in your OpenClaw agent and handles the block / queue / retry flow:
# One-time install
openclaw plugins install npm:@secr/openclaw-plugin
export SECR_AGENT_TOKEN=secr_agent_xxx
Now whenever the agent calls any tool — including ones you didn't write yourself — the plugin handles the gate:
agent → github.delete_repo({ repo: "acme/old-staging" })
⤷ plugin (before_tool_call) → secr API → "approval_required"
⤷ tool call blocks; admin sees pending entry in dashboard
[ admin clicks Approve, or taps Approve on Telegram, or your
webhook receiver POSTs the approval back ]
agent → github.delete_repo({ repo: "acme/old-staging" }) (retry)
⤷ plugin → server consumes grant atomically → tool runs
The grant is one-shot — a second retry blocks again because the row's consumed_at is already populated.
If you can't use the plugin (custom runtime, embedded), the same gate is available as a programmatic wrapper via the @secr/openclaw SDK's OpenClawGateway.wrap(...) — same atomic-consume semantics, you just call gateway.wrap(toolName, fn) yourself.
Real-time approval delivery
The dashboard shows pending counts but doesn't ping you. For real-time delivery you have two options out of the box:
- Telegram approvals — one-tap Approve/Deny inline keyboard on a phone the on-call already watches. Bot token encrypted server-side.
- Approval webhooks — generic
mcp.approval_requiredandmcp.approval_decidedevents. Slack, PagerDuty, Discord, custom — HMAC-verified, idempotency-keyed.
Most teams wire one or both. SMS and email are too slow for production approval flows.
What it doesn't do
A few honest limits:
- No bulk-approve. Each pending call is decided individually. If you have a high-frequency tool that always needs approval, you're better off rethinking the rule than building bulk-approve.
- No conditional auto-approve. "Approve
delete_repoif the repo is in the staging org" isn't a feature. The rule is binary per tool name.
Each of those is a follow-up. The current shape is enough to get an honest human-in-the-loop story for the 5–10 dangerous tools an OpenClaw deployment actually has.
What should be approval-required?
A working starting list:
- Anything destructive without easy undo —
delete_repo,drop_table,cancel_subscription,terminate_instance - Anything with financial impact —
refund,transfer_funds,purchase_*,change_billing - Anything customer-visible —
send_email,post_to_socials,update_marketing_page - Anything that touches credentials —
rotate_api_key,create_user,grant_permission - Shell-style escape hatches —
shell.exec,eval,run_arbitrary
A working not-required list:
- Read-only operations
- Idempotent updates that you've already audited (e.g.,
update_user_profilewith a known schema) - Operations the agent does dozens of times per day where a human-in-the-loop would defeat the purpose
When in doubt, mark it approval-required. It's much easier to remove an approval gate than to recover from an action you wish had been gated.
The summary
Approval queues are the difference between "the agent acted on its own authority" and "the agent acted with explicit, audited human approval." For dangerous tools, you want the second.
The setup: one gateway rule, one wrap call, one dashboard tab. The retry semantics — atomic consume, time-bounded, cross-agent isolated — handle the concurrency safety that this kind of flow normally gets wrong.
If your OpenClaw agent calls any tool that mutates production state, this is the post you read first.
Read next
- Approve OpenClaw tool calls from Telegram — one-tap inline keyboard
- Approval webhooks for OpenClaw agents — Slack, PagerDuty, Discord
- Detecting shadow OpenClaw agents — finding agents running without a proper credential
- The /agents pillar — threat model + every integration in one place
- The OpenClaw plugin — install reference, three hooks, ClawHub listing
Ready to get started?
Stop sharing secrets over Slack. Get set up in under two minutes.
Create your account