OpenClaw is great at automating the dull parts of our workday, but nobody wants a bot that can drain the company card or wipe /var/www without asking first. This guide walks through the exact knobs and switches you need to flip to enforce a human-in-the-loop approval workflow for any irreversible or high-risk action. We will:

  • Define what OpenClaw calls a “sensitive action”.
  • Enable the core approval gatekeeper.
  • Create trust levels so not every Slack ping needs a CEO sign-off.
  • Pair your agent with a direct-message (DM) approver.
  • Ship three opinionated examples: sending email, making a Stripe purchase, deleting files.
  • Finish with audit logging and a few gotchas I learned the hard way.

Why bother? Risk, compliance, and plain old peace of mind

If you self-host OpenClaw you can technically give it shell and browser superpowers that rival a junior engineer. Handy? Yes. Terrifying? Also yes. The community has seen at least one case of rm -rf ~/Downloads because a prompt went sideways. Enforcing approvals solves three problems at once:

  • Safety: Prevents accidental or prompt-injection-induced disasters.
  • Compliance: Many teams need dual control (SOC 2, PCI-DSS, ISO 27001).
  • Accountability: Every approved action is stamped with who, when, and why.

OpenClaw v0.44.0 (Node 22+) ships an optional @openclaw/approvals module that is off by default. The rest of this post assumes you upgrade to at least that version:

npm i -g openclaw@^0.44 && claw --version # should print 0.44.x

Turning on the approval middleware

First, enable the middleware globally in gateway.config.json. The gateway is where all external calls leave the system, so it’s the choke point we want.

{ "middleware": { "approvals": { "enabled": true, "storage": "file", // or "postgres", "dynamodb" "filepath": "~/.claw/approvals.log" } } }

Hot-reload is supported, but I prefer a clean restart so the daemon picks up the change:

claw daemon restart

Classifying actions: categories, scopes, and trust levels

OpenClaw ships with six baked-in categories. You can add your own, but try the defaults first:

  • read_only – safe operations (fetch calendar events)
  • email_send – anything that leaves your inbox
  • purchase – Stripe, PayPal, Apple Pay
  • file_write – creates or overwrites data
  • file_delete – destructive filesystem ops
  • shell_exec – arbitrary shell commands

Each category maps to a trust level integer. Lower numbers mean stricter controls.

{ "approvals": { "trustLevels": { "root": 0, "admin": 1, "trusted_user": 2, "observer": 3 } } }

Then bind categories to the minimum trust required to auto-approve. Anything below triggers a human request.

{ "approvals": { "rules": { "email_send": { "autoApprove": "trusted_user" }, "purchase": { "autoApprove": "admin" }, "file_delete": { "autoApprove": "root" }, "shell_exec": { "autoApprove": "root" } } } }

In the snippet above, a normal trusted_user can let marketing blasts fly without pinging anyone, but the moment the agent tries rm -rf or purchases something in Stripe, an approval notice is dispatched.

Wiring the DM pairing system

The next question is who gets that notification. OpenClaw’s DM pairing lets you route approval requests to a human via Telegram, Slack DM, Signal, or even plain email. Pairing is one-time and uses a short lived six-digit code.

  1. Generate the pairing token: claw approvals pair --user alice --channel slack --expires 10m # output: Pairing code for alice: 138-229
  2. Message your bot in Slack: pair 138-229
  3. Gateway logs show: [approvals] alice paired via slack (trusted_user)

You can pass --role admin during pairing if Alice should be able to self-approve category purchase. All pairings are stored in ~/.claw/approvals.json (change the path via the storage block earlier).

Example 1 – Requiring manual OK before any email goes out

Step 1: declare the tool action scope

OpenClaw uses Composio integrations. The Gmail tool exposes two scopes: gmail.read and gmail.send. We’ll map gmail.send to the email_send category.

{ "tools": [ { "name": "gmail", "scopes": [ { "name": "gmail.read" }, { "name": "gmail.send", "category": "email_send" } ] } ] }

Step 2: set auto-approval to null

{ "approvals": { "rules": { "email_send": { "autoApprove": null // always ask } } } }

Step 3: see it in action

> openclaw> "Send a thank-you email to bob@example.com for yesterday's meeting" # Bot response (pending): # "Awaiting approval: send email to bob@example.com with subject 'Thanks for the chat'"

Paired approvers get a DM:

[OpenClaw] Do you approve sending email to bob@example.com? (reply YES or NO)

Replying YES stamps the request approved_at=2024-05-04T14:17Z and the agent resumes.

Example 2 – Dual control on Stripe purchases

Purchasing is sticky because we need two distinct people (4-eye principle). The approval engine supports a minimumApprovers field.

{ "approvals": { "rules": { "purchase": { "autoApprove": false, "minimumApprovers": 2, "timeout": "30m" } } } }

Tip: set a timeout or the request may block the agent indefinitely.

The first approver replies YES and sees:

[OpenClaw] 1/2 approvals recorded. Need 1 more.

After the second approver confirms, the Stripe API call fires. If the window expires, the agent receives an ApprovalTimeoutError.

Example 3 – Blocking file deletes unless a root approver signs off

This one hurt me a month ago. My bot got confused and called rm -rf ~/Library/Caches. I restored from Time Machine but decided to lock things down.

  1. Mark file_delete as autoApprove: false.
  2. Set minimumRole: "root" so only people paired as root can OK it.
{ "approvals": { "rules": { "file_delete": { "autoApprove": false, "minimumRole": "root" } } } }

If a junior team member tries “clean up unused screenshots” the gateway pauses and DM’s only the root approvers. Everyone else gets a read-only notification.

Centralised audit logging and alerts

Approvals are useless if you can’t prove them later. By default the middleware logs JSON lines to approvals.log:

{"ts":"2024-05-04T14:17:38Z","action":"email_send","who":"openclaw","target":"bob@example.com","status":"approved","approved_by":["alice"],"latency_ms":8234}

For real ops use a proper sink. Two lines in config ship logs to Loki:

{ "middleware": { "approvals": { "storage": "loki", "endpoint": "https://loki.internal:3100" } } }

Grafana dashboards from the community repo dashboards/approvals_overview.json show MTTA (mean time to approval) and top requesters. Hook the status=denied stream into PagerDuty if you want an on-call fire drill when an approver slaps NO.

Testing your policy before prod

OpenClaw ships a stub runner so you can simulate calls without touching real Gmail or Stripe:

claw approvals test --scenario demos/purchase.json

The test harness feeds predefined agent actions and shows where approvals would trigger, letting you iterate on categories and trust levels fast.

An excerpt of a test case:

{ "actions": [ { "type": "purchase", "amount": 49.00, "currency": "USD", "description": "Mailchimp credit" } ], "expectedApprovers": 2, "timeoutMs": 180000 }

Common pitfalls I ran into

  • Forgetting scope mapping. If you don’t label the tool scope, the engine can’t tell that gmail.send should be gated.
  • Stuck agents. If nobody is paired or all approvers are asleep, the agent hangs forever. Use timeout generously and catch the error.
  • Role escalation by re-pairing. By default any paired user can run claw approvals pair --role root. Lock this down with --requireRootToken and share the token offline.
  • Multiple gateways. In HA setups each gateway needs the same storage backend or you’ll get diverging approval states.

What goes to production?

Here’s a trimmed but realistic production snippet that has kept my startup out of trouble the past month. Tweak to taste.

{ "middleware": { "approvals": { "enabled": true, "storage": "postgres", "dsn": "postgres://claw:secret@10.0.0.5:5432/claw", "rules": { "email_send": { "autoApprove": "trusted_user", "timeout": "5m" }, "purchase": { "autoApprove": false, "minimumApprovers": 2, "timeout": "45m" }, "file_write": { "autoApprove": "admin" }, "file_delete": { "autoApprove": false, "minimumRole": "root" }, "shell_exec": { "autoApprove": false, "minimumRole": "root" } }, "trustLevels": { "root": 0, "admin": 1, "trusted_user": 2, "observer": 3 } } }, "tools": [ { "name": "gmail", "scopes": [ { "name": "gmail.read" }, { "name": "gmail.send", "category": "email_send" } ] }, { "name": "stripe", "scopes": [ { "name": "stripe.charge", "category": "purchase" } ] } ] }

Deploy, restart the daemon, and run a smoke test:

claw agent call "Send a $1 test charge to test@example.com"

Your paired Slack should light up. Decline it and watch the agent gracefully handle ApprovalDeniedError.

Next steps: tighten, monitor, iterate

Approval workflows won’t save you if nobody actually reviews the requests. Rotate approvers, prune stale pairings monthly, and feed your audit logs into whatever SIEM you already trust. The beauty of OpenClaw’s design is you can iterate without redeploying your agent logic: tweak gateway.config.json, restart, and you’re done.

If you hit a weird edge case, open an issue on GitHub. The maintainer Peter merged my Loki sink patch within 24 hours. And if you’re running on ClawCloud instead of self-hosting, the same YAML is available under Settings → Security → Approvals, no CLI needed.

Humans make mistakes; bots make them faster. Add the brake.