DEV Community

Mahiro Hirakawa
Mahiro Hirakawa

Posted on

Three gotchas installing a plugin into someone else's agent

Three gotchas installing a plugin into someone else's agent

This one is scoped tight: getting a plugin into OpenClaw's gateway so it loads, registers, and shows up next to the bundled providers. Nothing here about whether a real model's tool call actually reaches it; that part is a separate piece, with its own log lines and receipts. Three things broke on the way, and every one of them turned out to be OpenClaw doing something sensible that I hadn't accounted for.

OpenClaw is steipete and vincentkoc's agent runtime, npm-distributed, MIT-licensed, version 2026.8.1 at the time of this install. The plugin was a small one, a before_tool_call handler wrapping a reversibility layer called gx.

One: the mount itself gets checked

I first pointed OpenClaw's config at the plugin file sitting where I'd been developing it, under /mnt/c/... in WSL. Discovery found it fine. Loading it did not:

Config warnings: plugins: plugin openclaw-entry: blocked plugin candidate:
world-writable path (/mnt/c/.../src, mode=777)
Enter fullscreen mode Exit fullscreen mode

WSL's DrvFs mount exposes the Windows filesystem with permissions that come out world-writable by default, mode 777 on everything. OpenClaw's plugin loader won't load a candidate from a path where any process on the box could have modified the code between discovery and load. That's not a bug, it's a reasonable refusal: don't trust code sitting on a mount with no real access control. I moved the plugin directory onto the WSL-native ext4 filesystem and tightened the permissions with chmod -R go-w, and the same discovery step that had warned before now passed clean.

Two: "enabled" and "loaded at boot" are different claims

With the world-writable warning gone, openclaw plugins list reported the plugin as enabled, and openclaw plugins doctor said discovery and module loading both passed. I ran the gateway anyway and read the boot log line by line. My plugin wasn't in it. Fourteen bundled providers, no gx.

The answer was one line inside OpenClaw's own installed distribution bundle: sidecar: record.activation?.onStartup === true. A plugin only gets loaded automatically when the gateway starts if its manifest declares activation: { onStartup: true }. A bare .ts file registered through plugins.load.paths, with no manifest at all, is something the loader can find and inspect, but it was never a candidate for startup activation in the first place. "Enabled" describes the config entry; whether the gateway actually instantiates the thing at boot is a separate question, and the CLI answers them with two different commands, not one. I hadn't separated those two claims before this broke.

The fix was to stop treating a loose file as a plugin and build one properly. I ran OpenClaw's own scaffold command, openclaw plugins init, looked at the three files it generated, openclaw.plugin.json with activation.onStartup: true, a package.json, and an entry file, and wired my existing code in as imports rather than rewriting it. After that, the gateway's boot log listed gx-escrow next to anthropic, browser, memory-core, and the rest.

Three: the installer runs your code before you get to

openclaw plugins install <dir> --link doesn't just copy files. It imports the plugin module and calls register(api) as part of the install step itself, which means a broken registration fails loudly, right away, with the real exception:

TypeError: tool hook matcher must be an array of tool names
Enter fullscreen mode Exit fullscreen mode

I'd written the hook registration as api.on("before_tool_call", handler, { matcher: { tools: [...] } }), an object with a tools key, because that's the shape I'd guessed at while designing the plugin, before ever running it against a real installer. The actual signature wants a bare array. One word changed, matcher: [...] instead of matcher: { tools: [...] }, and the install completed without exception, printing its own confirmation: register(api) called -- before_tool_call handler registered.

Worth reading afterward: openclaw plugins doctor --json came back with a compatibility note I hadn't asked for, "code": "hook-only", "message": "is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet." That's OpenClaw telling me, unprompted, that the pattern I'd just gotten working is the older of two supported shapes. I haven't read what the newer one looks like yet, so I'm not calling this plugin built the modern way, only that it's built a way OpenClaw still explicitly supports.

One more, smaller and entirely my own fault: while debugging the second gotcha, I set plugins.allow to a short list expecting it to add permissions. It doesn't; it's a strict allowlist, and the moment I set it, thirty-nine bundled providers that had been running fine got disabled along with everything I hadn't named. config unset plugins.allow undid it. Nobody's manual failed me there; I just hadn't read closely enough before typing the command.

None of these three needed OpenClaw's source read to fix. Two came from the installer's own error text, one from a single grep hit in the installed bundle. plugins doctor --json and plugins inspect <id> gave me more structured, more honest diagnostic output than gx has for its own plugin, and that's a comparison worth taking seriously the next time I build tooling for this side of the project.

Repo: github.com/TraceFold/tracefold, Rust, Apache-2.0. Alpha: latest tag v0.1.2-alpha (2026-09-01), published on crates.io as tracefold 0.1.2. Still alpha, and the limits page is longer than the feature list on purpose.

Top comments (0)