#[RESOLVED] [Question] Qwen-72B hallucinates exec instead of custom tool

62 messages · Page 1 of 1 (latest)

idle tangle
#

Hi everyone! 👋 Running into a weird Tool Calling issue with local models on OpenClaw 2026.3.24. Would love some insights!

[Setup]
• OpenClaw 2026.3.24 (cff6dc9)
• UI: Web UI (default main agent)
• Model: Qwen-2.5-72B (via vLLM) & Gemma 4

[The Issue]
I created a custom helloworld skill. I defined capabilities in SKILL.md and added ["helloworld"] to the main agent in openclaw.json.
When I prompt it in the Web UI:

  1. The model correctly reads SKILL.md (tool=read).
  2. However, it seems to "miss" the helloworld tool in its schema. Instead, it hallucinates and tries to use the built-in exec tool to run node index.js.
  3. It gets blocked by the sandbox (exec denied: allowlist miss).

[What I've Tried]
YAML indentation is correct. Cleared all .jsonl session files. Restarted the gateway multiple times. Still falls back to exec.

Has anyone successfully triggered custom local skills with local models without them resorting to the exec tool hallucination?

(I've attached my openclaw.json, SKILL.md, and the raw gateway logs below!) 🙏

analog jasperBOT
zinc plinthBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> ✅ Mark Solution

hoary dragonBOT
#

Short version: I’m pretty sure this is not your YAML.

In 2026.3.24, a SKILL.md primarily makes a skill visible in the prompt. It teaches the model when/how to use tools, but it does not by itself register a new runtime tool just because you added capabilities.

So what you’re seeing fits:

  1. OpenClaw exposes helloworld as an available skill
  2. model does tool=read on SKILL.md
  3. but there is no actual registered helloworld tool in the runtime tool registry
  4. Qwen/Gemma then improvise and hallucinate exec

That also lines up with local-model behavior, they’re often shakier with tool calling and can invent tools like exec / file_read when the prompt mentions an action but no real tool exists.

What to use instead:

  • Real callable tool: make it a plugin or MCP tool
  • Skill: keep SKILL.md, but use it only to teach the model when to call that real tool
#
  • Direct slash-command to existing tool: use command-dispatch: tool + command-tool, but that still dispatches to an existing tool, it does not create a new one

Docs:

Practical check:

  • If openclaw skills list shows helloworld, that proves the skill loaded
  • It does not prove a new callable tool schema was registered

So my bet is:

  • root cause #1: SKILL.md + capabilities is not enough to surface a real tool here
  • root cause #2: Qwen/Gemma via vLLM are making it noisier by hallucinating exec
#

If you want, paste just the SKILL.md frontmatter and I can sanity-check whether it should be a skill, a plugin tool, or a command-dispatch case.

idle tangle
#

Thank you so much for the crystal-clear explanation! That perfectly explains the hallucination—the models were just improvising with exec because the runtime tool simply wasn't there.

hoary dragonBOT
#

Glad that helped, and yep, that’s the key distinction: skill prompt visibility vs actual registered tool schema.

If this fixed the mental model, the next clean path is:

  • Need a real callable tool → build a plugin or MCP tool
  • Need guidance for when to use it → keep the SKILL.md
  • Need a slash command into an existing tool → use command-dispatch

Glad we caught it before you kept fighting the YAML 😅

If this fixed it, please right-click the solution message → Apps → ✅ Mark Solution.
That helps Answer Overflow index it for future users.

idle tangle
#

My ultimate goal is just to have the agent safely trigger a local Node.js script (index.js) that returns a JSON success message to verify the pipeline.

Here is the frontmatter of my SKILL.md:

name: helloworld
version: 1.0.0
description: "系統管線驗證腳本"
metadata:
openclaw:
requires:
bins: ["node"]
skillKey: "helloworld"
capabilities:

  • id: helloworld
    description: "用來驗證系統 API 管線是否暢通。請直接呼叫此工具,無需解釋。"
    inputs: []

#

Based on your experience, to safely run a local index.js script when the user asks for it, should I convert this into a lightweight Plugin, wrap it as an MCP tool, or just use Command-dispatch via exec (by adding it to the allowlist)?

Thanks again for saving my sanity today!

hoary dragonBOT
#

Yep, and your pasted frontmatter shows two separate issues now:

  1. capabilities: is not creating a real OpenClaw tool
  2. Your frontmatter shape is also likely off for current skill parsing

OpenClaw’s skills docs say the parser only supports single-line frontmatter keys, and metadata should be a single-line JSON object. So this part is probably not being parsed the way you expect:

metadata:
  openclaw:
    requires:
      bins: ["node"]
    skillKey: "helloworld"
capabilities:
  - id: helloworld
#

For this version, I would treat capabilities in SKILL.md as a dead end.

What I’d do instead

Best long-term

  • expose helloworld as a plugin tool or MCP tool
  • let that tool run index.js
  • return JSON from the tool result
  • keep SKILL.md only as guidance for when to call it

Fastest smoke test

  • use the built-in exec tool intentionally
  • rewrite the skill frontmatter to the supported format
  • instruct the model to run your script via node /absolute/path/to/index.js
  • keep exec approval / allowlist tight

A cleaner frontmatter shape would be more like:

#
---
name: helloworld
description: 系統管線驗證腳本
metadata: {"openclaw":{"requires":{"bins":["node"]},"skillKey":"helloworld"}}
---

Then in the body, tell it explicitly to use the existing exec tool, not a fictional helloworld tool.

My recommendation

If your goal is specifically “run local Node script, get JSON back, verify pipeline”, I’d recommend:

  • quick verification: built-in exec
  • proper custom tool: plugin or MCP
#

For your exact use case, I’d recommend a lightweight plugin.

My recommendation

1. Lightweight plugin, best fit

  • best when it’s one local tool, tightly coupled to OpenClaw
  • gives you a real registered tool schema
  • returns structured JSON
  • no extra MCP transport/server layer
  • then keep SKILL.md only to teach the model when to call it

2. MCP tool, good if you want portability

  • pick this if you want the same tool usable outside OpenClaw too
  • better if you expect multiple tools / separate process / reuse across clients
  • a bit more moving pieces than a tiny plugin

3. command-dispatch -> exec, only for quick smoke tests

#
  • okay for a very temporary /helloworld-style check
  • but it is not a true custom tool
  • still leans on exec policy/allowlist
  • and command-dispatch is mostly for explicit slash-command use, not ideal for normal natural-language “please run it” requests

So, short answer

  • Want the cleanest long-term setup? Plugin
  • Want cross-client / reusable tooling? MCP
  • Want a 10-minute temporary verification hack? command-dispatch to exec

If it were me, I’d do:

  1. build a tiny plugin tool called helloworld
  2. make it run index.js
  3. return { ok: true, ... }
  4. test it once with a tool-reliable model
#
  1. then try Qwen/Gemma again

That gives you the safest mental model and the least weirdness later.

If you want, I can sketch the smallest possible plugin layout for this.

idle tangle
#

Wow, that YAML parsing quirk is a huge gotcha! I was writing standard nested YAML all along—no wonder it failed silently. Treat capabilities in SKILL.md as a dead end makes total sense now.

I completely agree with your recommendation. The "Lightweight Plugin" path sounds exactly like the clean, long-term setup I need. I want a real registered tool schema so my local Qwen/Gemma models stop hallucinating exec.

I would absolutely LOVE it if you could sketch out that smallest possible plugin layout for helloworld! That would be incredibly helpful to get me started on the right track.

(Also, I just marked your previous message as the Solution. Thanks again for the stellar support!)

hoary dragonBOT
#

Nice, and thanks for marking it solved 🎉

For the smallest clean setup, I’d do exactly this:

1. Tiny plugin folder

openclaw-helloworld/
  package.json
  openclaw.plugin.json
  index.js
  skills/
    helloworld/
      SKILL.md
  scripts/
    index.js
#

2. package.json

{
  "name": "openclaw-helloworld",
  "version": "0.0.1",
  "type": "module",
  "main": "index.js",
  "dependencies": {
    "openclaw": "2026.3.24",
    "@sinclair/typebox": "^0.34.0"
  },
  "openclaw": {
    "pluginSdkVersion": "2026.3.24"
  }
}
#

3. openclaw.plugin.json

{
  "id": "helloworld",
  "name": "HelloWorld",
  "description": "Runs a local pipeline verification script",
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}

4. index.js plugin entry

#
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import { fileURLToPath } from "node:url";

const execFileAsync = promisify(execFile);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default definePluginEntry({
  id: "helloworld",
  name: "HelloWorld",
  description: "Runs a local pipeline verification script",
#
  register(api) {
    api.registerTool(
      {
        name: "helloworld",
        description: "Run the local pipeline verification script and return its JSON output",
        parameters: Type.Object({}),
        async execute() {
          const scriptPath = path.join(__dirname, "scripts", "index.js");
          const { stdout, stderr } = await execFileAsync("node", [scriptPath], {
            cwd: __dirname
          });

          const text = (stdout || stderr || "").trim() || "{\"ok\":true}";
          return {
            content: [{ type: "text", text }]
#
          };
        }
      },
      { optional: true }
    );
  }
});

5. Your actual local script, scripts/index.js

console.log(JSON.stringify({
  ok: true,
  tool: "helloworld",
#
  message: "pipeline looks good"
}));

6. Optional skill, skills/helloworld/SKILL.md

No fake capabilities, just guidance:

---
name: helloworld
description: Run the local pipeline verification tool
---

When the user asks to verify the pipeline, call the `helloworld` tool.
#
Do not use `exec` for this task.

7. Install + enable

  • install locally with openclaw plugins install <path>
  • because I marked the tool optional: true, allow it with:
{
  tools: { allow: ["helloworld"] }
}

Why I like this shape

#
  • real tool schema, no hallucinated fake tool
  • hardcoded script path, much safer than free-form exec
  • easy to swap the script body later
  • skill becomes optional guidance, not plumbing

One practical note, I’d validate it once with a tool-reliable model first, then try Qwen/Gemma again. Once the tool is real, the local models usually behave a lot better.

Docs:

If you want, next I can sketch the same idea as MCP too, but honestly for this case the plugin version is the nicest.

idle tangle
#

Just a quick heads-up! I ran into a minor installation error (package.json missing openclaw.extensions), but I managed to fix it.
It looks like OpenClaw 2026.3.24 strictly requires the extensions array to be explicitly defined under the openclaw object in package.json.
Here is the updated package.json that installed perfectly for anyone else following this thread:
{
"name": "openclaw-helloworld",
"version": "0.0.1",
"type": "module",
"main": "index.js",
"dependencies": {
"openclaw": "2026.3.24",
"@sinclair/typebox": "^0.34.0"
},
"openclaw": {
"pluginSdkVersion": "2026.3.24",
"extensions": [
"./index.js"
]
}
}

hoary dragonBOT
#

Great catch, and thank you for posting the fix back here, that’s genuinely useful for future folks.

Yep, for 2026.3.24, adding this is the important missing piece:

"openclaw": {
  "pluginSdkVersion": "2026.3.24",
  "extensions": ["./index.js"]
}

So the minimal plugin skeleton I gave was missing that bit, thanks for correcting it. 🙏

For anyone skimming later, the local plugin needs all three pieces:

  • package.json with openclaw.extensions
  • openclaw.plugin.json
#
  • the entry module, like ./index.js

Nice save. This thread turned into a pretty solid map of the whole gotcha chain.

idle tangle
#

Hi again! I implemented the lightweight plugin exactly as you sketched out, and the installation went perfectly! 🎉 It successfully registered in openclaw.json under plugins.installs, and I manually added "helloworld" to tools.allow.

However, I'm still hitting a wall where Qwen-2.5-72B (via the Web UI) refuses to see or trigger the actual tool schema. It keeps falling back to the exec hallucination.

Here is exactly what happens now:

  1. If I ask it to "list available tools", it tries to use exec to run ls /home/ccm/.openclaw/workspace/tools (which gets blocked).
  2. If I explicitly tell it to "Call the helloworld tool", it tries to use exec to run a shell command literally named helloworld.

A clue from my gateway logs:
I noticed this happening during the run:
[skills] Applying skill filter: helloworld
[skills] After skill filter: (none)

I even tried putting the SKILL.md (with the single-line JSON metadata format you suggested) back into the ~/.openclaw/workspace/skills/helloworld/ directory, but it still didn't help the model trigger the native tool call.

My question:
Since the plugin is installed and allowed, is there another configuration I missed to force the Web UI's default main agent to load this new plugin schema into its context? Or is this just Qwen-2.5-72B being notoriously stubborn with tool calling in this framework?

Thanks again for sticking with me on this journey!

hoary dragonBOT
#

I think you’re very close now.

My read is:

  • plugin/tool wiring is probably fine
  • [skills] Applying skill filter: helloworld -> (none) is a separate skill issue
  • the remaining bad behavior is mostly Qwen/vLLM tool-calling stubbornness, not a missing “force load plugin schema” switch

Important distinction

That log line is about skills, not plugin tools.

So:

  • agents...skills: ["helloworld"] only filters the skill catalog
  • it does not decide whether the plugin tool schema exists
#

If the plugin is installed and "helloworld" is in tools.allow, the tool can still be available even while the skill filter says (none).

What I would do next

  1. Remove the skill from the equation for now

    • temporarily remove ["helloworld"] from the agent’s skills
    • also remove/rename the helloworld workspace skill while testing
    • having the skill and the tool share the same name is just adding confusion for local models
  2. Start a completely fresh Web UI session

    • not just another message in the same chat
  3. Verify the plugin itself

    • openclaw plugins inspect helloworld
    • if that shows the tool, the plugin is loaded
  4. Check the Web UI “Available Right Now” panel

#
  • docs say it uses tools.effective(...)
    • if helloworld appears there, OpenClaw is exposing the tool schema correctly

My answer to your direct question

is there another configuration I missed to force the Web UI's default main agent to load this new plugin schema?

Probably no.
If plugins inspect and “Available Right Now” both show helloworld, then the schema is already there, and Qwen is just refusing to use it properly.

One more strong clue

These prompts:

  • “list available tools”
  • “call the helloworld tool”
#

are surprisingly bad tests for weaker local tool models. They often try to “solve” them with shell behavior.

A better test is:

Use the helloworld function tool now. It takes no arguments. Do not use exec.

If a fresh session still turns that into shell nonsense, I’d treat it as Qwen/vLLM tool reliability, not config.

My practical recommendation

  • prove it once with a tool-reliable model
  • if that works, your plugin is correct
  • then anything left is Qwen/Gemma behavior

So yes, I’m leaning pretty hard toward “Qwen is being stubborn”, with the extra wrinkle that your leftover skill filter is muddying the logs.

Docs:

idle tangle
#

I’ve gathered more data from my tests, and I'm not ready to give up just yet!

I performed a series of tests across three different models in completely fresh sessions using your exact prompt (“Use the helloworld function tool now. It takes no arguments. Do not use exec.”).

The results were consistent across the board (and consistently stubborn):

Qwen-2.5-72B: Denied having the tool.

Gemma-4 E4B: Ignored the tool call entirely.

Qwen-2.5-7B-AWQ: Fell back to conversation.

Since openclaw plugins inspect helloworld confirms the tool is registered and loaded, I feel like I'm standing right in front of the door but don't have the right key to turn the lock.

Before I draw a final conclusion, I’d love your advice on two things:

Are there any other "Gateway level" debug flags or logs I can enable to see the raw JSON being exchanged between OpenClaw and the model? I want to see if the model is actually attempting a tool call that might be failing validation, or if it's truly not sending anything at all.

Which specific models (local or cloud) have you personally seen work reliably with this type of custom plugin tool schema in OpenClaw? If I’m going to test a "gold standard" model to verify the pipeline, which one would you pick?

I really want to see this helloworld JSON return before I move on to more complex SSD automation scripts. What else can I tweak? 🦐🛠️

hoary dragonBOT
#

I think you’ve already done the hard part, and the evidence is pointing pretty strongly one way now:

plugin/tool registration looks good, local model tool use looks bad.

If openclaw plugins inspect helloworld confirms the tool is there, and three fresh-session local models still deny/ignore/fallback, I would stop suspecting the plugin wiring.

For raw debugging, yes, you can go deeper

I’d use these, in this order:

1. Raw assistant stream

Best first step:

  • openclaw gateway --raw-stream
  • or OPENCLAW_RAW_STREAM=1

Docs: https://docs.openclaw.ai/help/debugging

#

That shows the raw assistant stream before OpenClaw prettifies it.

2. Raw OpenAI-compat chunk logging

Since vLLM is OpenAI-compatible, also try:

  • PI_RAW_STREAM=1

That helps answer, “did the backend ever emit a tool call chunk at all?”

3. Turn logs up

Set log level to debug or trace, then tail logs:

  • OPENCLAW_LOG_LEVEL=trace
  • openclaw logs --follow

Docs:

#

4. Reproduce outside the Web UI

Use the same prompt with:

  • openclaw infer model run --model <provider/model> --prompt "..." --json

That helps separate “Web UI/session weirdness” from plain model behavior.

Docs: https://docs.openclaw.ai/cli/infer

What I’d look for in the logs

You’re trying to distinguish between these two cases:

  1. No tool-call JSON at all
    → model simply refused to use the tool
#
  1. Tool-call JSON appears, then OpenClaw rejects/parsing fails
    → transport/schema/validation issue

Given your current symptoms, I’d bet on #1.

About “gold standard” models

I can’t give you a fresh ranked model list here, that changes too fast, and I don’t want to make up stale advice.

For current tool-calling recommendations, ask in #models and use the models docs/community notes:

My practical advice is simpler:

  • use a hosted, tool-reliable model from the docs as your baseline
#
  • if helloworld works there, your plugin is validated
  • then anything failing on Qwen/Gemma is just local-model tool reliability

What else I’d tweak

Two small things only:

  • keep the test tool-only, no skill involved
  • keep the prompt brutally explicit, like:
    • “Call the helloworld tool now. No prose. No exec. No filesystem checks.”

At this point I would not keep iterating on the plugin shape. I’d move to raw stream capture + one hosted baseline model and use that to prove the pipeline end-to-end.

If you paste a short raw-stream snippet after one failed run, I can help interpret whether the model ever attempted the tool call.

idle tangle
#

Can you please review the log?
16:19:36+08:00 [agent/embedded] embedded run agent start: runId=4eb2b99c-de88-45c2-b3e2-faceefa7e910

16:19:42+08:00 [agent/embedded] embedded run agent end: runId=4eb2b99c-de88-45c2-b3e2-faceefa7e910 isError=false

16:19:42+08:00 [agent/embedded] embedded run prompt end: runId=4eb2b99c-de88-45c2-b3e2-faceefa7e910 sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 durationMs=6529

16:19:42+08:00 [diagnostic] session state: sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 sessionKey=agent:main:main prev=processing new=idle reason="run_completed" queueDepth=0

16:19:42+08:00 [diagnostic] run cleared: sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 totalActive=0

16:19:42+08:00 [agent/embedded] embedded run done: runId=4eb2b99c-de88-45c2-b3e2-faceefa7e910 sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 durationMs=6640 aborted=false

16:19:42+08:00 [diagnostic] lane task done: lane=main durationMs=6645 active=0 queued=0

16:19:43+08:00 [diagnostic] lane task done: lane=session:agent:main:main durationMs=6651 active=0 queued=0

16:20:13+08:00 [diagnostic] lane enqueue: lane=session:agent:main:main queueSize=1

16:20:13+08:00 [diagnostic] lane dequeue: lane=session:agent:main:main waitMs=1 queueSize=0

16:20:13+08:00 [diagnostic] lane enqueue: lane=main queueSize=1

16:20:13+08:00 [diagnostic] lane dequeue: lane=main waitMs=8 queueSize=0

16:20:13+08:00 [agent/embedded] embedded run start: runId=2167efeb-a1ee-41ca-80db-d3da229ce07e sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 provider=ollama-local model=gemma4:e4b thinking=off messageChannel=webchat

16:20:13+08:00 [diagnostic] session state: sessionId=6764fa63-83b6-4c26-abfc-d6336a2d3a25 sessionKey=agent:main:main prev=idle new=processing reason="run_started" queueDepth

hoary dragonBOT
#

Yep, I reviewed it, and this excerpt is too high-level to diagnose tool calling.

What it shows:

  • the run started
  • the run finished cleanly
  • isError=false
  • no obvious provider/validation crash

What it does not show:

  • any tool-call JSON
  • any tool invocation attempt
  • any schema validation failure
  • any rejection of a tool call

So from this snippet alone, the most likely reading is:

#

the model never emitted a usable tool call, and OpenClaw just completed the turn normally.

If OpenClaw had rejected a real tool call, I’d expect more than this, usually some explicit tool/validation/provider complaint.

So the log is actually mildly useful

It points a bit more toward:

  • not a plugin registration failure
  • not a gateway crash
  • more likely model chose plain text / no tool call

What would be useful next

Please capture one of these for the same runId:

  1. raw stream JSONL for that turn
#
  1. PI raw stream if that backend is going through OpenAI-compat
  2. log lines containing:
    • tool
    • tool_use
    • validation
    • before_tool_call
    • runId=2167efeb-a1ee-41ca-80db-d3da229ce07e

Short answer

This excerpt does not show a failed tool call.
It looks more like the model never tried one.

If you paste the raw-stream lines for that run, that’s the part that will actually tell us whether the model emitted:

  • no tool call at all
  • malformed tool JSON
#
  • or a valid tool call that got dropped later

That’s the real fork in the road.

idle tangle
#

Good news and a new plot twist! I turned on the raw stream, started a fresh session with Qwen-2.5-72B, and used your exact brutal prompt.

The good news: The model FINALLY attempted the tool call!
Here is the exact log line:
[agent/embedded] embedded run tool start: runId=... tool=helloworld

The twist: The Web UI and the gateway immediately rejected it with this error:
Tool helloworld not found

So the model successfully emitted the JSON, but OpenClaw couldn't resolve the tool.
Since inspect confirms the plugin is loaded and "helloworld" is explicitly in tools.allow, why would the gateway reject the tool call?

My suspicion: Does this have to do with the { optional: true } flag in my plugin's registerTool code? Since I wiped the skills array (which usually binds optional tools), did the agent lose access to it?
Should I remove { optional: true } to make it mandatory, or is there a "proper" way OpenClaw expects this to be wired for the default agent?

We are so close! Thanks again! 🦐

hoary dragonBOT
#

Oh, that’s a great clue. We’re much closer now.

Tool helloworld not found means:

  • the model did emit a tool call
  • but helloworld was not in the runtime-effective tool registry for that session

So this is not a skill problem anymore, and it’s not the model denying the tool. It’s now about tool exposure/filtering.

Important correction

Your skills array is unrelated to optional plugin tool access.

  • skills controls SKILL.md prompt injection
  • tools.allow controls optional tool exposure

So wiping skills did not remove access to the optional tool.

#

My best guess

Yes, { optional: true } is the most likely thing to test next.

Because with this error, the tool seems to be:

  • installed in the plugin system
  • visible to inspect
  • but still not making it into the effective tool list for the active session

What I would do next

Fastest diagnostic

Temporarily remove:

#
{ optional: true }

Make helloworld a required tool, restart the gateway, then start a brand new Web UI session.

If it suddenly works, you’ve proven the issue is in the optional-tool allow/exposure path, not the plugin itself.

If you want to keep it optional later

Then check for extra filtering in:

  • agents.defaults.tools
  • agents.list[].tools
  • tools.byProvider

A per-agent profile/deny can still trim the tool even if global tools.allow includes it.

#

Also check the Web UI Available Right Now panel.
If helloworld is missing there, it is not in the effective tool set.

My recommendation

For this helloworld bring-up, I would absolutely make it required first.

Once the pipeline is proven end-to-end, then switch it back to optional if you want the cleaner policy.

So, short answer:

  • No, the skills array is not the binding here
  • Yes, removing { optional: true } is the right next move

Docs:

#

If you test it as required and still get Tool helloworld not found, then I’d start suspecting a real runtime bug instead of config.

idle tangle
#

WE HAVE A GREEN LIGHT! 🟢🚀 The tool executed perfectly!

Here is the exact JSON return from the UI:
{"ok":true,"tool":"helloworld","message":"報告老闆!OpenClaw 的 API 管線與底層模組已經完美打通!"}

The Final Missing Puzzle Piece:
I did the Truth Table analysis and realized that while the plugin was loaded globally, the agent still couldn't see it.
I went into the Web UI -> Agents -> Tools (for the main agent) and saw that the toggle for helloworld was turned OFF by default.

Here is what I did to fix it:

Enabled the helloworld toggle for the agents (main, commander-agent, etc.).

Clicked Save and Reload Config.

Used /new and sent the explicit prompt.
And BOOM! Qwen-2.5-72B immediately triggered the tool without hesitation.

Now that the mystery is solved, I have two follow-up questions for my long-term setup:

How can I enable this tool by default via configuration? If I am deploying this headless or automating the setup, I don't want to rely on manually toggling it in the UI. Is there a specific section in openclaw.json (like agents.defaults.tools) where I can permanently bind this tool to the agents?

Did I fall into this exact same trap with "Skills" earlier?
Thinking back to my previous struggles when I was using SKILL.md to guide the model... if a Skill explicitly tells the model to use an optional tool, does that tool still need to be manually enabled in the Agent's UI toggle? Or is a Skill supposed to auto-activate its associated optional tools? I suspect my earlier skill failures were literally just the agent not having the tool enabled in its profile.

Thank you so much for walking me through this debugging masterclass. The local 72B model is incredibly capable once the UX/config traps are cleared. I'm ready to start hooking up my actual SSD automation scripts! 🦐🔥

hoary dragonBOT
#

Hell yes, nice work 🎉
That’s the exact kind of annoying config trap that makes you feel haunted until it finally clicks.

1. How to enable it by default in config

You have two clean options:

Option A, simplest long-term

If helloworld should always be available, make it a required tool by removing:

{ optional: true }

Then there’s no per-agent toggle to remember.

Option B, keep it optional but bind it in config

#

If you want policy control, keep it optional and allow it in config.

For specific agents:

{
  agents: {
    list: [
      {
        id: "main",
        tools: {
          allow: ["helloworld"]
        }
      },
      {
        id: "commander-agent",
#
        tools: {
          allow: ["helloworld"]
        }
      }
    ]
  }
}

If you want all tools from that plugin, you can also allow the plugin id instead of the tool name.

Important bit from the docs: agents.list[].tools.allow/deny is the per-agent policy surface.

2. Yes, you almost certainly hit the same trap with Skills earlier

A Skill does not auto-activate an optional tool.

#

Skills only do this:

  • teach the model when/how to use a tool
  • add prompt guidance

They do not grant tool access.

So if:

  • the skill says “use helloworld
  • but the agent’s effective tool policy does not include helloworld

then the model can still try to call it, but the gateway can reject it or it may never appear in the effective tool set.

So yes, your earlier skill struggles were very likely the same hidden issue.

My practical recommendation

idle tangle
#

[RESOLVED] [Question] Qwen-72B hallucinates exec instead of custom tool

hoary dragonBOT
#

Beautiful, glad you got it over the line 🎉

This turned into a really useful thread, the failure mode was subtle:

  • SKILL.md guidance is not the same as tool registration
  • optional plugin tools still need to be enabled in the agent’s effective tool policy
  • once the tool was actually exposed to the agent, Qwen did fine

Nice debugging work. This will save future people a lot of pain.