Human-in-the-loop for OpenClaw

Human approval for OpenClaw tool calls, on a phone.

A before_tool_call hook holds risky OpenClaw tools until your user approves them on their phone.

What OpenClaw gives you

The before_tool_call hook

An OpenClaw plugin can register a before_tool_call hook. It runs before the tool, and returning { block: true, blockReason } stops the call and tells the model why. That is the right place for an approval, because it blocks the action instead of reporting it afterwards.

Where it stops

  • The hook has no way to reach a person who is away from the gateway.
  • It has no wait that survives a restart while someone decides.
  • It keeps no record of what was allowed or blocked, or who decided.

The fix

Pushary gets your user's answer.

before_tool_call can stop a tool. decisions.ask() inside it sends the question to your user's phone, waits for Approve or Deny, and saves the answer. The hook blocks on anything but a yes.

The plugin gates three tools: deploy_service, delete_record and send_email. For those, the hook calls decisions.ask() and returns { block: true } unless your user approves. Every other call skips the hook and runs untouched. Every answer is saved.

bash
npm i @pushary/server
two calls
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"
import { createPusharyServer } from "@pushary/server"

const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
const GATED = new Set(["deploy_service", "delete_record", "send_email"])

export default definePluginEntry({
  id: "pushary-approvals",
  name: "Pushary approvals",
  register(api) {
    api.on("before_tool_call", async (event) => {
      if (!GATED.has(event.toolName)) return
      const d = await pushary.decisions
        .ask({ question: `Allow ${event.toolName}?`, externalId: "user_123", type: "confirm" })
        .catch(() => ({ approved: false }))
      if (!d.approved) {
        return { block: true, blockReason: "A person did not approve this action." }
      }
    })
  },
})

How it works

From install to the first answer

Connect your user once. Put the approval on the risky step. Pushary sends the question, waits for the answer and saves it.

  1. 01

    Install and set your key

    Run npm i @pushary/server in your plugin workspace and set PUSHARY_API_KEY.

  2. 02

    Connect your user once

    One SDK call returns a connect link. The person who approves opens it on their phone with the free Pushary app. No account, no payment.

  3. 03

    Register the hook

    In your plugin entry, register a before_tool_call hook that calls decisions.ask() for the tool names you gate.

  4. 04

    Block on anything but a yes

    Return { block: true } with a blockReason unless the answer is an explicit Approve, so a no or a timeout stops the call.

A question can be a yes or no, a choice from a list, or a typed answer. Every answer is saved.

Before you ship

Things that bite on OpenClaw

Ask in the hook, not in a tool
A tool the model chooses to call is optional, and a prompt that says “ask first” is not a gate. before_tool_call runs on every call whether the model wants it or not, so the approval belongs there.

The Partner plan

Your agent asks your users, not just you

Ship your agent to your users. Each of them approves its risky actions from their phone, under your brand.

$99/ month, every framework included
Start free

Your users never make a Pushary account or pay us.

Your users answer on their own phones

Connect each user once with a link. They install the free Pushary app and answer from the lock screen, with no account of their own.

No answer means no

Run the action only after an explicit Approve. A request that is still waiting, expired or cancelled never counts as a yes. Pushary keeps every request, so your code can pick it up again after a restart.

Reaches them on their phone

The app comes first, because only the app puts Approve and Deny on the lock screen. Browser notifications come next. You build no delivery system.

Every answer saved

Every answer is saved with who answered and when. You can search and export it, and with a callback URL, each answer is also sent to your server as a signed webhook.

FAQ

Questions, answered

Is there a @pushary/openclaw package?
No, and you do not need one. An OpenClaw hook is a plain async function, so calling decisions.ask() from @pushary/server inside before_tool_call is the whole integration. The same package connects each user's phone, and the same two calls work in any framework.
How do I block a tool call?
Return { block: true, blockReason } from the before_tool_call hook. The block is final: the tool does not run, and the model is told why. Write the blockReason in plain words, like “Your user declined the deploy”, so the model stops instead of trying the same action another way.
What happens on a timeout?
The call is stopped. decisions.ask() reports the request as not approved after 55 seconds by default, your hook returns block, and the tool does not run. Pushary keeps the request, so a late answer is still saved.
Can I gate only some tools?
Yes. Check event.toolName in the hook and return early for anything routine. Most setups gate deploys, deletes, payments and outbound messages, and let everything else run. Asking about every call teaches people to tap Approve without reading.
Who approves the call?
Whoever the user id in the call points at. For your own OpenClaw instance, that is you. For an agent serving your users, pass each user's own id so the person the action affects decides. They see your product's name in the notification and tap Approve or Deny on the lock screen.

Let your OpenClaw agent ask before it acts.

Connect a user once, ask with one call, and run the action only on a yes. Every answer is saved.

Start free

Partner plan, $99 a month. Your users never make a Pushary account or pay us.