Back to Writing

Silent Plugin, Eight Layers Deep

March 9, 2026 · Peleke (ed: Claude)

The Problem

OpenClaw’s built-in tools are limited. If you want to extend the agent with new capabilities, you write a plugin. We needed one: the fork ships with a LinkedIn content pipeline (LinWheel), and the agent needed its tools to draft and schedule posts.

The naive path is “write a plugin, register the tools, done.”

…It is not done. Not even close.

The Symptom

The plugin registered its tools. The gateway started. The agent couldn’t see any of them. No error in the log. No warning. Nothing.

The gateway acted like the plugin didn’t exist. It gave literally zero indication why.

The Architecture

OpenClaw’s plugin system has eight layers between “plugin exists on disk” and “agent can use the tool”:

  1. Discoverydiscovery.ts scans extensions/ for plugin candidates
  2. Manifest loadingmanifest-registry.ts reads openclaw.plugin.json
  3. Enable stateconfig-state.ts checks if the plugin is enabled in config
  4. Plugin loadingloader.ts imports and executes the plugin’s register() function
  5. Optional tool gatingtools.ts filters tools marked optional: true against an allowlist
  6. Policy collectionpi-tools.ts builds the allowlist from cascading tool policies
  7. Policy filteringpi-tools.policy.ts applies allow/deny rules to the final tool set
  8. Sandbox tool policysandbox/constants.ts applies a final per-environment allowlist
8-layer plugin pipeline: layers 1-4 pass (green), layer 5 kills all 17 tools (red), layers 6-7 never reached (grey), layer 8 kills again (red)

The plugin made it through layers 1-4 perfectly: discovered, manifest loaded, enabled, all tools registered. Two bugs killed it after that. One at layer 5. One at layer 8.

The Root Cause

Here’s the concept: when a plugin marks its tools as optional: true, those tools get checked against an allowlist before they reach the agent. You set the allowlist with tools.alsoAllow. If you set it to ["*"], you’d expect that to mean “allow everything.”

It doesn’t. The function that checks the allowlist (isOptionalToolAllowed) does exact string matching against a Set. It looks for "linwheel_draft", "linwheel_schedule", etc. The "*" string sits in the Set, matching nothing. It’s a valid entry that has no effect.

Elsewhere in the codebase, * works fine. The policy filtering layer (7) compiles it into { kind: "all" }, a proper wildcard.1 But optional tools never reach layer 7. They’re silently dropped at layer 5, where the wildcard is just a string nobody checks for.

I started calling this a semantic wildcard, syntactic literal: a filter that means “allow everything” to the system that produces it, and means nothing to the function that consumes it.

The Fix

Two lines: add if (params.allowlist.has("*")) return true; to isOptionalToolAllowed.

The Second Bug

Fixing layer 5 didn’t fix the problem. The agent still couldn’t see the tools.

Layer 8: the sandbox tool policy.2 DEFAULT_TOOL_ALLOW in sandbox/constants.ts is a hardcoded allowlist of 15 core tools. No plugin tools. No wildcard. Every plugin tool that survived layers 1-7 dies silently at layer 8.

The fix: add a config override for sandbox tools.

After both fixes: all LinWheel tools visible.

The Method

No errors. No warnings. The wildcard worked everywhere else. Infrastructure restarts wiped config entries, so the same fix had to be reapplied repeatedly. And the two bugs masked each other: you couldn’t see the second until you’d fixed the first.

The approach that worked: isolate each layer on the VM, feed it known input, check what comes out. Layer 5 ate the wildcard. Layer 8 ate the survivors. Both silently, both by design.

Debugging recipe: isolate each layer, check input to output, find the disappearance point. Layers 1-4 pass, layer 5 kills, layers 6-7 pass, layer 8 kills again.

Footnotes

  1. This is the same tool policy system that controls which tools are available on messaging channels. The messaging profile that would have prevented the SOUL.md poisoning exploit lives at layer 7. Defined but never selected.

  2. The sandbox layer (8) that silently drops plugin tools is the same layer the qlawbox sandbox operates at. The hardcoded allowlist has no extension point for plugins.