← Back to Plugins
Integration

Clawperm

clawnify By clawnify 👁 82 views ▲ 0 votes

OpenClaw plugin for in-chat policy approvals — chat-native approve/deny prompts.

Homepage GitHub

Install

openclaw plugins install @clawnify/clawperm

Configuration Example

{
  "plugins": {
    "allow": ["clawperm"],
    "load": {
      "paths": ["./workspace/clawperm"]
    },
    "entries": {
      "clawperm": {
        "config": {
          "secret": "<openssl rand -hex 32>",
          "agentId": "main",
          "defaultTimeoutMs": 540000
        }
      }
    }
  },
  "approvals": {
    "plugin": {
      "enabled": true,
      "mode": "session",
      "agentFilter": ["main"]
    }
  }
}

README

<p align="center">
  <!-- Drop a 1280×640 hero at .github/hero.png — keyword in the headline. -->
  <img src="./.github/hero.png" alt="Clawperm — in-chat policy approvals for OpenClaw" width="800" />
</p>

# Clawperm — in-chat policy approvals for OpenClaw

Chat-native approve/deny prompts for OpenClaw agents. POST a prompt over loopback HTTP, the user replies with `/approve plugin:<id> ...` in their existing webchat / Slack / Telegram / WhatsApp thread, the CLI gets the decision back.

<p align="center">
  <a href="https://github.com/clawnify/clawperm/blob/main/LICENSE"><img alt="MIT License" src="https://img.shields.io/github/license/clawnify/clawperm?color=blue" /></a>
  <a href="https://github.com/clawnify/clawperm/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/clawnify/clawperm?style=social" /></a>
  <a href="https://github.com/openclaw/openclaw"><img alt="OpenClaw" src="https://img.shields.io/badge/openclaw-plugin-purple" /></a>
  <a href="https://www.npmjs.com/package/@clawnify/clawperm"><img alt="npm" src="https://img.shields.io/npm/v/@clawnify/clawperm.svg" /></a>
</p>

## What it does

Lets an external CLI (or any local process) raise an approval prompt **in the agent's active chat session**. The user sees the prompt where they're already talking to the agent and replies inline. The caller blocks on a single HTTP request and gets back `allow-once` / `allow-always` / `deny` / `null` (timeout).

Built for [Clawnify](https://clawnify.com), open-sourced because every OpenClaw operator who ships a "policy gate" CLI hits the same wall.

## The wall it solves

OpenClaw exposes `plugin.approval.request` + `waitDecision` so any plugin can ask the user for approval. Delivery to chat works via `approvals.plugin.mode = "session"` — but only when the call carries a `sessionKey` so the gateway knows which conversation to forward into.

A CLI invoked as a child of the agent's `exec` tool **does not** get session context. Only `OPENCLAW_SHELL=exec` is set; there is no `OPENCLAW_SESSION_KEY`. Calling `openclaw gateway call plugin.approval.request` from that child opens an independent operator-token RPC — the gateway sees an admin request with no session link, the approval lands with `sessionKey: null`, and `mode: "session"` forwarding has nowhere to deliver it.

Clawperm sidesteps this by running **inside** the gateway process. It exposes a loopback HTTP route on the gateway's HTTP server. The CLI POSTs to that route; the plugin emits the approval through the in-process plugin API, which is session-aware.

## Install

```bash
openclaw plugins install @clawnify/clawperm
# or, from a local checkout:
openclaw plugins install ./extensions/clawperm
```

The plugin ships TypeScript directly — `openclaw.extensions: ["./index.ts"]`. The host loads it; no build step.

## Quickstart

Generate a shared secret and add the plugin to `openclaw.json`:

```bash
openssl rand -hex 32   # → use this for `secret` below and CLAWPERM_SECRET in the caller's env
```

```json
{
  "plugins": {
    "allow": ["clawperm"],
    "load": {
      "paths": ["./workspace/clawperm"]
    },
    "entries": {
      "clawperm": {
        "config": {
          "secret": "<openssl rand -hex 32>",
          "agentId": "main",
          "defaultTimeoutMs": 540000
        }
      }
    }
  },
  "approvals": {
    "plugin": {
      "enabled": true,
      "mode": "session",
      "agentFilter": ["main"]
    }
  }
}
```

Restart the gateway. The plugin registers `POST /clawperm/approval` on the gateway's HTTP port (`gateway.port`, default `18789`).

## CLI contract

```http
POST http://127.0.0.1:18789/clawperm/approval
Authorization: Bearer <secret>
Content-Type: application/json

{
  "subject": "GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND",
  "title": "Run clawnify action GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND?",
  "description": "Toolkit: googlesheets\nArgs: { ... }",
  "timeoutMs": 540000
}
```

Response:

```http
200 OK
{ "decision": "allow-once" | "allow-always" | "deny" | null,
  "id": "plugin:cpa:..." }
```

`decision: null` means timeout — callers typically treat it as deny.

The endpoint blocks until the user resolves the approval (or `timeoutMs` elapses), so callers should set their HTTP timeout above `timeoutMs`. The gateway's hard cap is currently 10 minutes (600000 ms).

### Example: bash

```bash
curl -fsS http://127.0.0.1:18789/clawperm/approval \
  -H "Authorization: Bearer $CLAWPERM_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"subject":"GOOGLESHEETS_APPEND","title":"Append to sheet?","timeoutMs":540000}' \
  | jq -r .decision
```

## Config

| Field             | Type     | Default      | Notes                                                                 |
| ----------------- | -------- | ------------ | --------------------------------------------------------------------- |
| `secret`          | `string` | env fallback | Bearer token. Falls back to `CLAWPERM_SECRET` env var.                |
| `agentId`         | `string` | `"main"`     | Agent the approval is bound to.                                       |
| `defaultTimeoutMs`| `number` | `540000`     | Used when caller omits `timeoutMs`. Capped at 600000 by the gateway.  |

Treat the secret like any other shared secret — give the same value to the CLI via systemd env (`Environment=CLAWPERM_SECRET=...`) and to the plugin via `plugins.entries.clawperm.config.secret`.

## Security model

- **Loopback only.** The gateway HTTP server should already be bound to `127.0.0.1` (or to the host's private interface). Don't expose the gateway port publicly. `gateway.bind = "loopback"` is the recommended posture.
- **Bearer token.** `auth: "plugin"` means OpenClaw doesn't gate the route — clawperm enforces auth itself with a constant-time compare against the configured secret.
- **No body bigger than 64 KB.** Defensive cap; approvals are tiny.
- **No persistence.** Decisions are returned to the caller and forgotten. Persisting "allow-always" rules is the caller's responsibility (e.g. via your own policy DB).

## Development

There is no build step. The OpenClaw host loads `index.ts` directly. To iterate locally, point an `extensions/clawperm` symlink at this checkout and restart the gateway.

## Known gaps

- `openclaw/plugin-sdk/approval-runtime` is the **current best-guess** import path for `requestApproval` / `waitDecision`. If the SDK has moved them, the plugin will fail at first POST with a clear error pointing at this README and `src/plugin/index.ts`. Patch the import there.
- No retry logic — the loopback path is reliable, retries would just paper over real failures.

## Contributing

Issues and PRs welcome. Keep the surface area small — clawperm is intentionally one HTTP route and one approval shim. Anything bigger probably belongs in your own plugin.

## Related

Part of the Clawnify suite — managed OpenClaw agents for non-technical teams:

- [openclaw/openclaw](https://github.com/openclaw/openclaw) — the core agent runtime
- [clawnify/clawflow](https://github.com/clawnify/clawflow) — declarative workflows agents can read, write, and run
- [clawnify/clawvoice](https://github.com/clawnify/clawvoice) — voice agent plugin (give your agent a phone number)

## License

MIT — see [LICENSE](./LICENSE).

---

Built by the [Clawnify](https://clawnify.com) team.
integration

Comments

Sign in to leave a comment

Loading comments...