Approve OpenClaw Tool Calls from Telegram
Your CTO is on a plane and the only device they have is a phone. Tool-call approvals via Telegram inline keyboards turn that bottleneck into a one-tap decision — with full audit trail and per-org bot isolation.
The MCP gateway's approval queue catches risky tool calls and pauses for a human. That's the model. The friction is the human — they have to find a laptop, log into the dashboard, click decide. Most often, they aren't sitting at a laptop when the agent stops.
This post is about the mobile path. Connect a Telegram bot to your secr org, and approval requests arrive as messages with Approve / Deny inline keyboards. One tap, the agent unblocks within seconds, full audit trail.
A few design notes up front:
- Each org brings their own bot. secr doesn't host a shared bot. You create one with @BotFather, paste the token into secr, and the integration is fully isolated to your chats. If your token is leaked, only your org's approvals are affected.
- The bot token is encrypted at rest. secr stores it envelope-encrypted via your KMS, never in plaintext.
- Deny requires a note. Approve is one tap. Deny prompts for a reason via Telegram's force-reply, so the agent operator knows why.
Setup — five minutes, mostly waiting on Telegram
1. Create a Telegram bot
In Telegram, open a chat with @BotFather:
/newbot
> What should we call your bot?
acme-secr
> Pick a username
acme_secr_bot
BotFather replies with a token. Looks like 123456789:AAEhBP0av28k…. Save it.
2. Add the integration in secr
In the secr dashboard, Settings → Integrations → Telegram → Add bot. Paste the token. We call Telegram's getMe immediately to confirm it's valid; if it isn't, we tell you why.
After validation, the dashboard shows two things:
- A connect code (random 16-char string)
- A webhook URL specific to your integration (
/v1/telegram-webhook/<id>?s=<secret>)
3. Activate the webhook
Click Activate webhook. We register the URL with Telegram via setWebhook. You can also do it yourself with curl if you prefer — the dashboard shows the exact command.
4. Connect the chat
Open a chat in Telegram — could be a group with your team, could be a DM with the bot, could be a channel. Send:
/connect <your-connect-code>
The bot replies:
✅ Connected. Approval requests will appear here.
Bot: @acme_secr_bot
Chat: secr-ops
Done. The connect code is consumed; approval messages will flow to this chat.
What an approval looks like in Telegram
When an OpenClaw agent calls a tool that hits a require_approval rule:
🛑 Approval needed
Tool: github.delete_repo
Agent: support-bot
Environment: production
Source IP: 52.x.y.z
{
"repo": "acme/old-staging"
}
Approve below or open the dashboard.
[ ✅ Approve ] [ ❌ Deny ]
The agent itself is now blocked, waiting. Two outcomes:
- Tap Approve → instant. Bot edits the message:
✅ Approved by @alice at 14:42. The approval is recorded server-side; the agent's next retry consumes the grant. - Tap Deny → bot replies asking for a reason. Reply with the reason; bot edits the message:
❌ Denied by @alice at 14:42 — "Wrong staging repo, that's prod-staging". The note is stored in the audit trail. The agent gets back a clear error.
In both cases, the mcp.approval_decided webhook also fires (so PagerDuty / Slack / your internal tooling can reflect the outcome).
How the auth boundary works
The risk vector with chat-based approvals is someone in the wrong chat being able to decide. Three layers protect against that:
- Per-integration webhook secret. Each integration has its own URL with a
?s=<secret>query param. Telegram POSTs to that URL on every update. We reject any inbound POST without the matching secret. - Chat ID match. When a user taps Approve / Deny, Telegram tells us which chat the callback came from. We compare against the stored
chatIdon the integration. If they don't match, the callback is rejected — even if the user is in the bot's chat list. - Org scope. A callback only operates on tool calls in the integration's own org. Org A's bot cannot decide org B's calls, even if they share an admin.
What if the token leaks?
If your bot token escapes via .env commit or shoulder surfing:
- Go to BotFather and
/revokethe token immediately. The old token is dead. - In the secr dashboard, delete the integration (not just disable). This wipes the encrypted token from our DB.
- Create a new bot (or
/tokenin BotFather to regenerate for the existing one), add it as a fresh integration in secr, run/connect <new-code>from your chat.
Total downtime: a few minutes. Approval requests during that window queue in the dashboard as before.
The headless / ops case
If you'd rather automate the integration setup (e.g., bringing it up in IaC), the API surface is plain HTTP:
# 1. Create the integration
curl -X POST https://api.secr.dev/v1/integrations/telegram/$ORG_ID \
-H "Authorization: Bearer $SECR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Requested-With: secr" \
-d "{\"botToken\": \"$BOT_TOKEN\"}"
# Response includes connect_code + webhook_url
# 2. Activate the Telegram webhook
curl -X POST https://api.secr.dev/v1/integrations/telegram/$ORG_ID/$INTEGRATION_ID/activate-webhook \
-H "Authorization: Bearer $SECR_SESSION_TOKEN" \
-H "X-Requested-With: secr"
# 3. Send /connect <code> from your bot's chat (manual or via your bot programmatically)
The activate-webhook endpoint calls Telegram's setWebhook with the right URL on your behalf. If you'd rather call setWebhook yourself, the dashboard shows you the URL.
What this is not
A few honest limits to set expectations:
- Per-message resolution. Each approval message is for one call. There's no "approve everything from this agent for the next hour" mode — that would defeat the point of human-in-the-loop. Use a less-restrictive
require_approvalrule for that. - One chat per integration. You can have multiple integrations (different bots, different chats), but each integration delivers to exactly one chat. If you want multi-team routing (e.g. SRE-on-call vs. eng-leads), create separate integrations.
- Telegram-only outcome editing. When the bot edits the original message to show the decision, that's a Telegram convenience. The authoritative record is in the secr dashboard and the
mcp.approval_decidedwebhook. Don't rely on the message edit existing forever. - No "ask later" snooze. If you're not ready to decide, the call sits in the pending queue and the agent waits. Use the 10-minute grant TTL as a forcing function — you have 10 minutes to decide or the agent has to re-request.
Before you ship it
A couple of operational things worth thinking about:
Make a private group, not a DM. A group with your on-call team has redundancy. If only one person is in the loop and they're asleep, your agent waits.
Pair it with generic webhooks. Telegram is fast for the approve case; webhooks let you escalate via PagerDuty if nobody decides within N minutes. Both can run side by side — they're independent delivery channels.
Test with a no-op tool first. Mark something harmless as require_approval (a noop.echo tool you only use for testing, or secr.list_envs which only returns metadata), confirm the flow works end-to-end before turning the gate on for delete_repo or refund.
The whole thing is a 30-second tap when an OpenClaw agent stops in production. That's the goal — make the human-in-the-loop story require almost no human friction.
Ready to get started?
Stop sharing secrets over Slack. Get set up in under two minutes.
Create your account