npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clawnify/agent-permissions

v0.4.0

Published

OpenClaw plugin — permission and approval engine. Gates built-in tool calls (bash, file edit, etc.) and plugin tools via a three-bucket policy (allow/deny/ask) with wildcard rules, rule sources (session/workspace/user/config), and in-chat approval. No net

Readme

@clawnify/agent-permissions

OpenClaw plugin — permission and approval engine for any OpenClaw agent.

Gates built-in tool calls (bash, file edit, web fetch, etc.) and any plugin- registered tool through a three-bucket policy (allow / deny / ask), with rule sources walked in priority order, in-chat approval surfaced via OpenClaw's native requireApproval, and learning into allow-always rules.

MIT licensed. Maintained by Clawnify, designed for the wider OpenClaw ecosystem — works in any gateway with any agent setup.

Installation

openclaw plugins install @clawnify/agent-permissions --pin

Or via npm:

npm install @clawnify/agent-permissions

Then enable in openclaw.json:

{
  "plugins": {
    "allow": ["agent-permissions", "your-consumer-plugin"],
    "entries": {
      "agent-permissions": {
        "enabled": true,
        "config": {
          "defaultMode": "default",
          "ask": ["Bash(*)"],
          "deny": ["Bash(rm -rf /:*)"]
        }
      }
    },
    "load": {
      "paths": [
        "/path/to/agent-permissions",
        "/path/to/your-consumer-plugin"
      ]
    }
  }
}

agent-permissions must load before any consumer plugin (list it first in plugins.load.paths) so the registration API is available when consumer register() runs.

Why this exists

OpenClaw's built-in permission infrastructure today is:

  • Gateway-level exec-approval for bash — coarse, command-shape rules
  • registerTrustedToolPolicy — bundled-only; external plugins can't use it

Non-bundled plugins that want to gate their own tools (or gate other plugins' tools) have no host-level seam. They either reinvent approval per-plugin or ship without it. This engine fills that gap.

Single global before_tool_call hook + an extension point so any plugin can participate without each one rebuilding the policy / approval / learning loop.

What it does

| Capability | How | |---|---| | Gates any tool call in the gateway | Single global before_tool_call hook at priority 100 (verified upstream sort direction in src/plugins/hooks.ts:266) | | Built-in tools (bash, file edit, web fetch, …) supported out of the box | Generic resolver: shell tools (bash/exec) match against the actual command; everything else matches by tool name. No registration required. | | Other plugins' tools supported | Same generic path — operators add rules in openclaw.json targeting the tool name. Plugins don't need to know about us. | | Optional rich prompts | Consumer plugins MAY call registerResolver({ toolName, resolve }) for tool-specific prompt titles/descriptions. Opt-in. | | Three-bucket policy | allow / deny / ask evaluated against rule sources in priority order | | In-chat approval | Uses OpenClaw's native requireApproval — same UI as exec approvals | | Learning | allow-always resolutions persist to user/local/session as configured | | Wildcard rules | Tool(foo) exact, Tool(foo:*) legacy prefix, Tool(foo *) new wildcard | | Dangerous-pattern denylist | Patterns like python:*, node:*, eval cannot be allow-always-persisted | | Fail-closed | OpenClaw's hook runner catches exceptions and fails open — this plugin wraps every code path in try/catch and returns { block: true } instead |

allow-always semantic — operator-scoped, transparent

When a user clicks "always" on an approval prompt, the persisted rule is the pattern of the rule that triggered the ask, not the exact call. That means operators control the breadth at config time by choosing how specific their ask rules are:

"ask": ["clawnify_action(*_SEND*)"]   →  one "always" click allows ALL *_SEND* actions
"ask": ["clawnify_action(GMAIL_*)"]   →  one "always" click allows ALL Gmail actions
"ask": ["clawnify_action(GMAIL_SEND_EMAIL)"]  →  one "always" click allows only that slug

Each ask pattern → one possible "always" click → the rule is moved from ask to allow for future calls. Scales linearly with rule patterns, not with action count.

The prompt description shows the rule that will be persisted, so there's no surprise broadening:

Run clawnify_action (GMAIL_SEND_EMAIL)?

Params: {...}

Matched: rule 'clawnify_action(*_SEND*)' from config settings

'Always' will allow: `clawnify_action(*_SEND*)`

The dangerousPatterns denylist is checked against the rule that would be persisted — so if a matched rule contains a dangerous prefix (e.g. Bash(curl *)), allow-always is refused regardless of which specific call triggered the prompt.

If no rule matched (only happens under strict mode where everything asks), allow-always persists the exact call.

Per-tool content extraction (paramKeys)

For tools where the policy-relevant content lives in a param (e.g. Composio's clawnify_action takes { slug, args } and you want to gate on the slug), configure paramKeys in openclaw.json:

"agent-permissions": {
  "config": {
    "paramKeys": {
      "clawnify_action": "slug",
      "clawnify_call_app_api": "method"
    },
    "ask": [
      "clawnify_action(*_DELETE*)",
      "clawnify_action(*_SEND*)",
      "clawnify_call_app_api(POST)",
      "clawnify_call_app_api(DELETE)"
    ],
    "allow": [
      "clawnify_action(GMAIL_EMAIL_LIST)",
      "clawnify_action(GMAIL_EMAIL_GET)"
    ]
  }
}

With the map above:

  • clawnify_action({ slug: "GMAIL_SEND_EMAIL" }) → matches clawnify_action(*_SEND*) → asks
  • clawnify_action({ slug: "GMAIL_EMAIL_LIST" }) → matches the explicit allow → passes
  • clawnify_action({ slug: "GMAIL_EMAIL_DELETE" }) → matches clawnify_action(*_DELETE*) → asks

No consumer-plugin awareness needed. Wildcard semantics (* matches any chars), prefix legacy (foo:*), and exact matching all apply to the extracted content. Falls back to existing behavior (built-in extractor for shell tools, tool-wide otherwise) when no paramKeys entry exists.

Default modes

  • default (out of the box) — operator-opt-in: tools pass through unless an ask or deny rule explicitly matches. No surprises; you add rules for what you want gated.
  • strict — Claude-Code style: ask on anything not explicitly allowed. Opt-in for hard-gate setups.
  • bypassPermissions / dontAsk — allow everything except matching deny rules.
  • acceptEdits — currently behaves like default. Reserved for future tool-category-aware behavior (auto-allow edits within CWD).

What it does NOT do

  • Network calls. Storage is local files. Consumers that want cloud sync can hook onAllowAlwaysPersisted and mirror to their own backend.
  • Tool-specific logic. Each tool's rule-content + prompt text comes from a resolver, not from this engine. The engine is tool-agnostic.

Architecture

┌─── OpenClaw gateway process ────────────────────────────────────────┐
│                                                                     │
│  Consumer plugin (e.g. agent-tools, clawflow, third-party)          │
│   └─ on register():                                                 │
│        getAgentPermissionsApi().registerResolver({                  │
│          toolName: "some_tool",                                     │
│          resolve(params) {                                          │
│            return { ruleContent: "delete", title, description };    │
│          },                                                         │
│        })                                                           │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ agent-permissions (this plugin)                              │  │
│  │                                                              │  │
│  │  before_tool_call hook (priority: 100)                       │  │
│  │   ├─ resolver = resolvers.get(event.toolName)                │  │
│  │   ├─ req = resolver(event.params)                            │  │
│  │   ├─ decision = ruleEngine.evaluate(toolName, ruleContent)   │  │
│  │   ├─ bucket "deny"  → { block: true, blockReason }           │  │
│  │   ├─ bucket "ask"   → { requireApproval: {...} }             │  │
│  │   ├─ bucket "allow" → undefined (proceed)                    │  │
│  │   └─ try/catch wrapper → { block: true } on any error        │  │
│  │                                                              │  │
│  │  rule sources walked in priority order:                      │  │
│  │   1. session (in-memory, allow-always with scope:session)    │  │
│  │   2. local   (.openclaw/permissions.json in CWD)             │  │
│  │   3. user    (~/.openclaw/permissions.json)                  │  │
│  │   4. config  (pluginConfig.allow/deny/ask from openclaw.json)│  │
│  │                                                              │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

Rule format

Same shape as Anthropic's Claude Code permission system (studied as prior art).

| Rule | Meaning | |---|---| | ToolName | Tool-wide rule (any params match). | | ToolName(*) | Equivalent to tool-wide (empty / * content). | | Bash(npm install) | Exact match on (content). | | Bash(npm:*) | Legacy prefix syntax — matches npm, npm install, etc. | | Bash(git *) | Wildcard — * matches any chars. Trailing * makes trailing args optional, so git * matches both git add and bare git. | | Bash(python -c "print\\(1\\)") | Escape (, ) in content with \. Escape * with \*. Escape \ with \\. |

Dangerous patterns

dangerousPatterns config (defaults built in) lists prefixes that may match ask rules but can never be allow-always-persisted, even if the user clicks "allow always." Reason: granting Bash(python:*) = arbitrary code execution, which defeats the gate entirely.

Default list (conservative):

python python3 python2 node deno tsx ruby perl php lua
npx bunx npm run yarn run pnpm run bun run
bash sh zsh fish
eval exec env xargs sudo ssh
curl wget

Override dangerousPatterns in openclaw.json to extend or replace.

Inter-plugin API (runtime)

import { getAgentPermissionsApi } from "@clawnify/agent-permissions";

// In your consumer plugin's register():
const perms = getAgentPermissionsApi(); // throws if agent-permissions not loaded

perms.registerResolver({
  toolName: "my_dangerous_tool",
  resolve(params) {
    const p = params as { target?: string };
    return {
      ruleContent: "delete",
      title: `Delete ${p.target ?? "?"}?`,
      description: "This is irreversible.",
    };
  },
});

perms.onAllowAlwaysPersisted(async (event) => {
  // Optional: mirror to your own backend, audit, etc.
});

The plugin publishes its API on globalThis[Symbol.for("clawnify.agent-permissions.api.v1")], so consumers find it at runtime even when each plugin ships as an independent tarball with no shared node_modules. The getAgentPermissionsApi() helper wraps the Symbol lookup with a descriptive error if the plugin isn't loaded (typically a plugins.load.paths ordering issue).

Development

git clone https://github.com/clawnify/agent-permissions.git
cd agent-permissions
npm install
npm run build
npm test

Tests use Node's built-in test runner via tsx. No vitest/jest setup.

Releases

Tag a release on GitHub → .github/workflows/publish.yml runs npm publish --provenance.

License

MIT.


Initiated and maintained by the Clawnify team — AI agent hosting and orchestration.