Summary
Plugin-registered slash commands (api.registerCommand({ name: "remember", ... })) are correctly persisted to the central registry but never fire when the user runs openclaw agent --message "/remember ...". Both the --local (embedded) and gateway agent paths take the message verbatim and hand it to the LLM without ever calling the plugin command dispatcher. The TUI behaves the same way.
This is an OpenClaw core gap, not a plugin gap. Slash commands work fine in the channels that wire dispatch up themselves (Telegram, Discord, and any channel routed through the auto-reply pipeline), but the agent CLI surface is missing the equivalent hook.
Reproduction
With any plugin that registers a slash command (e.g. @maximem/memory-plugin exposes /remember and /recall):
$ openclaw agent --local --message "/recall favorite color" --to "+15555550999"
Expected: the /recall handler fires, returns its reply.
Actual: "/recall favorite color" is sent to the LLM as a regular user prompt. The slash never matches.
Confirmed at the source level on origin/main:
$ git grep -nE "executePluginCommand|matchPluginCommand" -- "*.ts"
extensions/discord/src/monitor/native-command.runtime.ts ← Discord channel
src/telegram/bot-native-commands.ts ← Telegram channel
src/auto-reply/reply/commands-plugin.ts ← generic auto-reply
src/plugins/commands.ts ← (declarations only)
Three call sites, all in channel-handling code. Neither src/commands/agent.ts (--local path → agentCommand) nor src/commands/agent-via-gateway.ts (gateway path → agentViaGatewayCommand) reference handleCommands, handlePluginCommand, matchPluginCommand, or executePluginCommand:
$ git grep -nE "handleCommands|handlePluginCommand|matchPluginCommand|executePluginCommand" \
src/commands/agent.ts src/commands/agent-via-gateway.ts
(no matches)
So when the user types --message "/<cmd>", the dispatcher is loaded in memory but nobody calls it on this path.
Why it matters
- Plugin authors can't dev-loop locally. Building any plugin that registers slash commands requires hooking up a real Telegram/Discord/etc. channel just to verify the command runs. There is no terminal-driven equivalent today.
- CI / smoke tests can't exercise slash flows. Verifying a plugin command via
openclaw agent --local is the natural minimum-viable integration test, but it can't observe the slash path.
- Inconsistent surface. TUI and the agent CLI silently send
/<cmd> to the LLM, which is surprising. Users reasonably expect a /-prefixed message to behave the same way it does in their channels.
Proposed fix
Call the existing handleCommands wrapper (the same one the auto-reply pipeline uses) in both agent paths, before the message is forwarded to the LLM. The wrapper already returns null when no plugin command matches, so non-slash messages flow through unchanged.
Sketch (pseudo-diff against src/commands/agent.ts near where body is sent into the agent runner):
import { handleCommands } from "../auto-reply/reply/commands.js";
// Inside agentCommand, after `body` is normalised:
const commandResult = await handleCommands({
command: {
commandBodyNormalized: body,
senderId: opts.to ?? "cli",
channel: opts.channel ?? "cli",
isAuthorizedSender: true,
},
cfg,
}, /* allowTextCommands */ true);
if (commandResult && !commandResult.shouldContinue) {
// Print/return the plugin command's reply and exit early.
runtime.log?.(commandResult.reply.text ?? JSON.stringify(commandResult.reply));
return;
}
// ...existing flow: hand `body` to the embedded agent runner.
The same insertion point applies in agentViaGatewayCommand — either dispatch locally before sending to the gateway, or have the gateway invoke handleCommands on inbound agent requests (cleaner, but a bigger change). Local dispatch keeps --local and gateway behaviourally aligned with channels that already do this themselves.
The same hook is also worth adding to the TUI input path so /<cmd> typed in the TUI fires the same handlers.
Acceptance
openclaw agent --local --message "/<registered-cmd>" invokes the registered handler and returns its reply.
openclaw agent --message "/<registered-cmd>" (gateway path) does the same.
- Messages that don't match any plugin command flow through to the LLM unchanged (verified by passing a plain prompt and confirming the LLM is invoked).
openclaw tui slash input fires plugin commands.
Happy to follow up with a PR if there's interest in the local-dispatch shape proposed above. The Telegram and Discord call sites are clean precedents to mirror.
References
src/commands/agent.ts — agentCommand (the --local entry)
src/commands/agent-via-gateway.ts — agentViaGatewayCommand (gateway entry)
src/auto-reply/reply/commands.ts — exports handleCommands
src/auto-reply/reply/commands-plugin.ts — handlePluginCommand implementation
src/plugins/commands.ts — matchPluginCommand / executePluginCommand declarations
src/plugins/registry.ts:422 — confirms plugin registrations land in the central registry correctly; the gap is only on the dispatch side.
Summary
Plugin-registered slash commands (
api.registerCommand({ name: "remember", ... })) are correctly persisted to the central registry but never fire when the user runsopenclaw agent --message "/remember ...". Both the--local(embedded) and gateway agent paths take the message verbatim and hand it to the LLM without ever calling the plugin command dispatcher. The TUI behaves the same way.This is an OpenClaw core gap, not a plugin gap. Slash commands work fine in the channels that wire dispatch up themselves (Telegram, Discord, and any channel routed through the auto-reply pipeline), but the agent CLI surface is missing the equivalent hook.
Reproduction
With any plugin that registers a slash command (e.g.
@maximem/memory-pluginexposes/rememberand/recall):Expected: the
/recallhandler fires, returns its reply.Actual:
"/recall favorite color"is sent to the LLM as a regular user prompt. The slash never matches.Confirmed at the source level on
origin/main:Three call sites, all in channel-handling code. Neither
src/commands/agent.ts(--localpath →agentCommand) norsrc/commands/agent-via-gateway.ts(gateway path →agentViaGatewayCommand) referencehandleCommands,handlePluginCommand,matchPluginCommand, orexecutePluginCommand:So when the user types
--message "/<cmd>", the dispatcher is loaded in memory but nobody calls it on this path.Why it matters
openclaw agent --localis the natural minimum-viable integration test, but it can't observe the slash path./<cmd>to the LLM, which is surprising. Users reasonably expect a/-prefixed message to behave the same way it does in their channels.Proposed fix
Call the existing
handleCommandswrapper (the same one the auto-reply pipeline uses) in both agent paths, before the message is forwarded to the LLM. The wrapper already returns null when no plugin command matches, so non-slash messages flow through unchanged.Sketch (pseudo-diff against
src/commands/agent.tsnear wherebodyis sent into the agent runner):The same insertion point applies in
agentViaGatewayCommand— either dispatch locally before sending to the gateway, or have the gateway invokehandleCommandson inboundagentrequests (cleaner, but a bigger change). Local dispatch keeps--localand gateway behaviourally aligned with channels that already do this themselves.The same hook is also worth adding to the TUI input path so
/<cmd>typed in the TUI fires the same handlers.Acceptance
openclaw agent --local --message "/<registered-cmd>"invokes the registered handler and returns its reply.openclaw agent --message "/<registered-cmd>"(gateway path) does the same.openclaw tuislash input fires plugin commands.Happy to follow up with a PR if there's interest in the local-dispatch shape proposed above. The Telegram and Discord call sites are clean precedents to mirror.
References
src/commands/agent.ts—agentCommand(the--localentry)src/commands/agent-via-gateway.ts—agentViaGatewayCommand(gateway entry)src/auto-reply/reply/commands.ts— exportshandleCommandssrc/auto-reply/reply/commands-plugin.ts—handlePluginCommandimplementationsrc/plugins/commands.ts—matchPluginCommand/executePluginCommanddeclarationssrc/plugins/registry.ts:422— confirms plugin registrations land in the central registry correctly; the gap is only on the dispatch side.