Plugins (Extensions)
Quick start (new to plugins?)
A plugin is just a small code module that extends WednesdayAI with extra features (commands, tools, and Gateway RPC). Most of the time, you’ll use plugins when you want a feature that’s not built into core WednesdayAI yet (or you want to keep optional features out of your main install). Fast path:- See what’s already loaded:
- Install an official plugin (example: Voice Call):
wednesdayai plugins install whatsapp resolves the catalog entry to @wednesdayai/whatsapp, installs it into ~/.openclaw/extensions/whatsapp, and installs runtime dependencies beside that managed copy. Install does not enable the plugin automatically; run wednesdayai plugins enable whatsapp when you are ready to activate it. Not every bundled plugin is published to npm yet.
- Restart the Gateway, then configure under
plugins.entries.<id>.config.
Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install
@openclaw/msteamsif you use Teams. - Memory (Core) — bundled memory search plugin (enabled by default via
plugins.slots.memory) - Memory (LanceDB) — bundled long-term memory plugin (auto-recall/capture; set
plugins.slots.memory = "memory-lancedb") - Voice Call —
@openclaw/voice-call - Zalo Personal —
@openclaw/zalouser - Matrix —
@openclaw/matrix - Nostr —
@openclaw/nostr - Zalo —
@openclaw/zalo - Microsoft Teams —
@openclaw/msteams - Google Antigravity OAuth (provider auth) — bundled as
google-antigravity-auth(disabled by default) - Gemini CLI OAuth (provider auth) — bundled as
google-gemini-cli-auth(disabled by default) - Qwen OAuth (provider auth) — bundled as
qwen-portal-auth(disabled by default) - Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in
github-copilotdevice login (bundled, disabled by default)
- Gateway RPC methods
- Gateway HTTP handlers
- Agent tools
- CLI commands
- Background services
- Optional config validation
- Skills (by listing
skillsdirectories in the plugin manifest) - Auto-reply commands (execute without invoking the AI agent)
- Provider runtime hooks for provider-specific request shaping
Runtime and turn identity
Plugins can correlate one user turn across hooks, tools, storage append events, model-call completion, and background analysis through typed SDK context fields. Use these fields instead of parsingsessionKey, reading identityLinks, or
reconstructing turn boundaries from timestamps or message text.
ctx.runtimeIdentity may include:
agentId,agentName,agentWorkspacePath, andagentWorkspaceNamesessionIdandsessionKeywhen a real session exists for the hookchannelConversationIdfor the transport conversation/threadproviderConversationIdfor the opaque model/provider cache partitionlaneId— the opaquelane-<hash>digest, present only for an allowed multi-lane run;undefinedfor single-user and denied resolutions (see Workspace Lanes)- trusted sender fields such as
senderId,senderName,senderUsername,senderE164,messageProvider,canonicalIdentity, andsenderIsOwner
ctx.turn carries the per-turn correlation key and storage linkage. In the v1
contract, turn.turnId is the same stable value as runId by value. Early hooks
receive the fields known at that point; later storage hooks may add
userEntryId, assistantEntryId, sequence numbers, or global sequence numbers
for the append that just happened.
Keep these identity boundaries clear:
conversationIdis a legacy alias forchannelConversationId; it is never the provider/model cache identity.providerConversationIdis intentionally opaque. Persist it only as an opaque correlation/cache key.sessionIdandsessionKeyare optional on early hooks. Do not synthesize fake session values when they are absent.- Core may use
session.identityLinksinternally, but plugins should consume only the resolved identity fields orapi.runtime.identity.resolveCanonicalIdentity(...).
turn.turnId alone. User and assistant
appends share one turn id, so combine it with entry or cursor identity:
runtimeIdentity and turn objects alongside the existing flat compatibility
fields. On the normal agent tool path, before_tool_call sees the same
foreground run identity that was used to construct plugin tool factories.
Workspace lane context
Plugins can receive optional workspace lane context when the current agent run or tool call is tied to an allowed workspace lane:workspaceLane.effectiveWorkspaceDir for lane-local user state
and workspaceLane.personaWorkspaceDir only for shared persona files. See
Workspace Lanes.
For per-lane storage partitioning, use ctx.runtimeIdentity.laneId rather than the raw
context.laneId. runtimeIdentity.laneId is undefined for single-user and denied resolutions
and present only for an allowed multi-lane run — so it is safe to use directly as a partition
discriminator without additional policy checks. The raw context.laneId and context.workspaceLane
fields are routing helpers and should not be used for storage keying.
message_received runs before workspace-lane resolution, so runtimeIdentity.laneId is absent
there. Message hooks should treat lane fields as absent on message_received.
Provider Runtime Hooks
Provider plugins can register model-call hooks withapi.registerProvider(...).
The hook context includes the selected provider/model, run/session identifiers, an
opaque conversationId, a hashed user identifier when one can be derived, and
the current stream options.
Supported provider-runtime hooks:
prepareExtraParams(ctx)returns a patch for the merged stream options before generic params are applied tostreamFn.wrapStreamFn(ctx)returns a stream wrapper after the existing core provider wrappers are installed.- Transport policy hook types are reserved in the SDK bridge for OpenClaw compatibility, but they are not yet delivered to the HTTP/WebSocket transport path. Provider plugins should not rely on transport-policy effects until the matching transport wiring is documented.
conversationId or hashedUserId to external services. Do not send raw
sessionKey values, channel ids, phone numbers, or user handles to LLM provider
metadata unless the provider contract explicitly requires them.
Runtime helpers
Plugins can access selected core helpers viaapi.runtime. For telephony TTS:
- Uses core
messages.ttsconfiguration (OpenAI or ElevenLabs). - Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
- Edge TTS is not supported for telephony.
- Uses core media-understanding audio configuration (
tools.media.audio) and provider fallback order. - Returns
{ text: undefined }when no transcription output is produced (for example skipped/unsupported input).
Discovery & precedence
WednesdayAI scans, in order:- Config paths
plugins.load.paths(file or directory)
- Workspace extensions
<workspace>/.openclaw/extensions/*.ts<workspace>/.openclaw/extensions/*/index.ts
- Global extensions
~/.openclaw/extensions/*.ts~/.openclaw/extensions/*/index.ts
- Bundled extensions (shipped with WednesdayAI, disabled by default)
<wednesdayai>/extensions/*
plugins.entries.<id>.enabled
or wednesdayai plugins enable <id>. Installed plugins are not activated by the
install step; enable them explicitly after install.
Bundled install records are zero-copy runtime references after reconciliation.
If a previous install copied a bundled plugin into ~/.openclaw/extensions/<id>,
OpenClaw can rewrite that record to source: "bundled" and remove the stale
default copy when it is safe to do so. A valid bundled record still participates
in config validation even if the plugin manifest registry cache was built before
the bundled source was rediscovered.
Hardening notes:
- If
plugins.allowis empty and non-bundled plugins are discoverable, WednesdayAI logs a startup warning with plugin ids and sources. - Candidate paths are safety-checked before discovery admission. WednesdayAI blocks candidates when:
- extension entry resolves outside plugin root (including symlink/path traversal escapes),
- plugin root/source path is world-writable,
- path ownership is suspicious for non-bundled plugins (POSIX owner is neither current uid nor root).
- Loaded non-bundled plugins without install/load-path provenance emit a warning so you can pin trust (
plugins.allow) or install tracking (plugins.installs).
openclaw.plugin.json file in its root. If a path
points at a file, the plugin root is the file’s directory and must contain the
manifest.
If multiple plugins resolve to the same id, the first match in the order above
wins and lower-precedence copies are ignored.
Package packs
A plugin directory may include apackage.json with openclaw.extensions:
name/<fileBase>.
If your plugin imports npm deps, install them in that directory so
node_modules is available (npm install / pnpm install).
Security guardrail: every openclaw.extensions entry must stay inside the plugin
directory after symlink resolution. Entries that escape the package directory are
rejected.
Security note: openclaw plugins install installs plugin dependencies with
npm install --ignore-scripts (no lifecycle scripts). Keep plugin dependency
trees “pure JS/TS” and avoid packages that require postinstall builds.
npm 12 and git-protocol dependencies
npm 12 disables fetching git-protocol dependencies by default (allow-git is
none). A plugin whose dependency tree still pulls a package over
git+https://... fails during wednesdayai plugins install with npm’s own
error, including:
npm install --omit=dev --loglevel=error --ignore-scripts
and appends npm’s stderr (or stdout if stderr is empty) to
npm install failed: ..., so that EALLOWGIT text reaches the operator.
Earlier installs used --silent, which hid the cause behind a bare
npm install failed:.
The WhatsApp channel — the installable plugin that carries runtime dependencies —
pins @whiskeysockets/baileys 7.0.0-rc14, which depends on the registry package
libsignal instead of a git-protocol libsignal, so it installs cleanly on npm 12.
One bundled extension still declares a git dependency: extensions/tlon pins
@tloncorp/api with a github: spec. It is not offered by wednesdayai plugins install, and bundled copies ship without installed runtime dependencies, so the
default install paths are unaffected — but installing it by hand on npm 12 fails
with EALLOWGIT unless the override below is used.
The right fix is to ask the plugin author to depend on a registry release.
If you must install a plugin whose dependency graph you have reviewed and trust,
scope the override to that one command rather than changing your user-wide npm
config:
npm config set allow-git all, which persists and re-enables
git fetching for every later install. Note that allow-git=root only permits
git specs declared directly in the package being installed, so it does not help
when the git dependency is transitive (the common case). WednesdayAI never sets
this for you; git fetching stays disabled by default.
Channel catalog metadata
Channel plugins can advertise onboarding metadata viaopenclaw.channel and
install hints via openclaw.install. This keeps the core catalog data-free.
Example:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.
Plugin IDs
Default plugin ids:- Package packs:
package.jsonname - Standalone file: file base name (
~/.../voice-call.ts→voice-call)
id, OpenClaw uses it but warns when it doesn’t match the
configured id.
Config
enabled: master toggle (default: true)allow: allowlist (optional)deny: denylist (optional; deny wins)load.paths: extra plugin files/dirsinstalls.<id>: CLI-managed install metadata.sourceis one ofnpm,archive,path, orbundled.entries.<id>: per‑plugin toggles + config
- Unknown plugin ids in
entries,allow,deny, orslotsemit startup/config warnings. Entries are treated as stale config and ignored until a matching plugin is installed or rediscovered; allow/deny/slot references include install guidance. - A
source: "bundled"install record is considered valid only when itssourcePathmatches the current bundled source for the same plugin id (string match or realpath-equivalent path), the manifest id matches, and the bundled plugin is version-compatible with the host. - Valid bundled install records participate in config validation even if the
normal discovery cache is stale. Their plugin config is validated against the
bundled
openclaw.plugin.jsonconfigSchema, and the schema cache key includes the manifest path plus file modification time. - Do not write
source: "bundled"records by hand in third-party plugin docs or setup scripts. Letwednesdayai plugins install,wednesdayai plugins update, or bundled reconciliation create and refresh managed install metadata. - Valid bundled install records suppress false
plugin not foundwarnings forentries,allow,deny, andslots.memory, but theirplugins.entries.<id>.configobject is still validated against the bundled manifest’sconfigSchema. - Unknown
channels.<id>keys are errors unless a plugin manifest declares the channel id. - Plugin config is validated using the JSON Schema embedded in
openclaw.plugin.json(configSchema). - If a plugin is disabled, its config is preserved and a warning is emitted.
Plugin slots (exclusive categories)
Some plugin categories are exclusive (only one active at a time). Useplugins.slots to select which plugin owns the slot:
kind: "memory", only the selected one loads. Others
are disabled with diagnostics.
Control UI (schema + labels)
The Control UI usesconfig.schema (JSON Schema + uiHints) to render better forms.
WednesdayAI augments uiHints at runtime based on discovered plugins:
- Adds per-plugin labels for
plugins.entries.<id>/.enabled/.config - Merges optional plugin-provided config field hints under:
plugins.entries.<id>.config.<field>
uiHints alongside your JSON Schema in the plugin manifest.
Example:
CLI
plugins update only works for npm installs tracked under plugins.installs.
Bundled plugins track the WednesdayAI package version instead. The
update command reconciles bundled install records first, so a copied bundled
record can become source: "bundled" before npm update selection runs.
If stored integrity metadata changes between updates, WednesdayAI warns and asks for confirmation (use global --yes to bypass prompts).
Plugins may also register their own top-level commands (example: wednesdayai voicecall).
Managed installs vs bundled copies
Bundled extensions live inside the installed WednesdayAI package and are discovered after explicit load paths, workspace extensions, and~/.openclaw/extensions. Treat that bundled directory as
read-only.
When a plugin has runtime dependencies, wednesdayai plugins install <id> copies it into the
managed extensions root ($OPENCLAW_STATE_DIR/extensions/<id>, normally
~/.openclaw/extensions/<id>) and installs dependencies beside that managed copy. Operators should
repair missing dependencies by reinstalling the plugin, not by running package-manager commands in
the global WednesdayAI package.
Dependency-free bundled plugins may run directly from the global package copy when enabled. A
healthy global bundled plugin should not produce PLUGIN_DEPS_MISSING diagnostics just because it
does not have a managed install record.
Plugin API (overview)
Plugins export either:- A function:
(api) => { ... } - An object:
{ id, name, configSchema, register(api) { ... } }
Durable file logs from plugins
Plugins that write durable diagnostic or audit files should use the shared file-log writer exported bywednesdayai/plugin-sdk instead of calling
fs.appendFile directly. The writer applies the same rotation, compression, and
queue-budget controls as core file logs, and operators can tune it through
logging.fileLogs.
- Use a stable, namespaced
profileId, such as<plugin-id>.auditorextensions.<plugin-id>.<log-name>. - Use
mode: "jsonl"for structured records and include one complete record per line. - Use
strict: trueonly when the caller must observe write or rotation failures. Non-strict writers keep logging best-effort and drop new writes if the queue exceedsmaxQueuedBytes. - Fire-and-forget writes should normalize the SDK return type before attaching a rejection handler:
createFileLogWriter, describeFileLogWriters, and the
public FileLog* types. Do not import src/logging/file-log-writer.ts from a
plugin; use wednesdayai/plugin-sdk so the plugin stays compatible with installed
WednesdayAI builds.
Operators can override plugin log limits with:
Plugin hooks
Plugins can register hooks at runtime. This lets a plugin bundle event-driven automation without a separate hook pack install.Example
- Register hooks explicitly via
api.registerHook(...). - Hook eligibility rules still apply (OS/bins/env/config requirements).
- Plugin-managed hooks show up in
openclaw hooks listwithplugin:<id>. - You cannot enable/disable plugin-managed hooks via
openclaw hooks; enable/disable the plugin instead.
Typed hook registration (api.on)
api.on is the preferred way to subscribe to lifecycle hooks when you want full TypeScript type safety. It uses the same PluginHookName union that drives the plugin SDK’s type system, so your handler receives a fully typed event and context.
opts.priority controls execution order when multiple hooks are registered for the same name. Higher numbers run first. The default is 0. Use a higher priority when your hook must run before others (for example, a security hook that can block a tool call should use a priority of 100 while a logging hook can stay at 0).
api.on vs api.registerHook. Use api.on when you want compile-time type checking on the event and context shapes — this is the preferred choice for new code. Use api.registerHook when you are subscribing to an event key from the HOOK.md internal hook system (e.g. command:new, session:start) that is not in PluginHookName, or when you need to pass a pre-built HookEntry object.
Available hook names and their event/context types are defined in PluginHookHandlerMap in the plugin SDK types:
Provider plugins (model auth)
Plugins can register model provider auth flows so users can run OAuth or API-key setup inside OpenClaw (no external scripts needed). Register a provider viaapi.registerProvider(...). Each provider exposes one
or more auth methods (OAuth, API key, device code, etc.). These methods power:
openclaw models auth login --provider <id> [--method <id>]
runreceives aProviderAuthContextwithprompter,runtime,openUrl, andoauth.createVpsAwareHandlershelpers.- Return
configPatchwhen you need to add default models or provider config. - Return
defaultModelso--set-defaultcan update agent defaults.
Register a messaging channel
Plugins can register channel plugins that behave like built‑in channels (WhatsApp, Telegram, etc.). Channel config lives underchannels.<id> and is
validated by your channel plugin code.
- Put config under
channels.<id>(notplugins.entries). meta.labelis used for labels in CLI/UI lists.meta.aliasesadds alternate ids for normalization and CLI inputs.meta.preferOverlists channel ids to skip auto-enable when both are configured.meta.detailLabelandmeta.systemImagelet UIs show richer channel labels/icons.
Channel onboarding hooks
Channel plugins can define optional onboarding hooks onplugin.onboarding:
configure(ctx)is the baseline setup flow.configureInteractive(ctx)can fully own interactive setup for both configured and unconfigured states.configureWhenConfigured(ctx)can override behavior only for already configured channels.
configureInteractive(if present)configureWhenConfigured(only when channel status is already configured)- fallback to
configure
configureInteractiveandconfigureWhenConfiguredreceive:configured(trueorfalse)label(user-facing channel name used by prompts)- plus the shared config/runtime/prompter/options fields
- Returning
"skip"leaves selection and account tracking unchanged. - Returning
{ cfg, accountId? }applies config updates and records account selection.
Write a new messaging channel (step‑by‑step)
Use this when you want a new chat surface (a “messaging channel”), not a model provider. Model provider docs live under/providers/*.
- Pick an id + config shape
- All channel config lives under
channels.<id>. - Prefer
channels.<id>.accounts.<accountId>for multi‑account setups.
- Define the channel metadata
meta.label,meta.selectionLabel,meta.docsPath,meta.blurbcontrol CLI/UI lists.meta.docsPathshould point at a docs page like/channels/<id>.meta.preferOverlets a plugin replace another channel (auto-enable prefers it).meta.detailLabelandmeta.systemImageare used by UIs for detail text/icons.
- Implement the required adapters
config.listAccountIds+config.resolveAccountcapabilities(chat types, media, threads, etc.)outbound.deliveryMode+outbound.sendText(for basic send)
- Add optional adapters as needed
setup(wizard),security(DM policy),status(health/diagnostics)gateway(start/stop/login),mentions,threading,streamingactions(message actions),commands(native command behavior)
- Register the channel in your plugin
api.registerChannel({ plugin })
plugins.load.paths), restart the gateway,
then configure channels.<id> in your config.
Agent tools
See the dedicated guide: Plugin agent tools.Register a gateway RPC method
api.registerGatewayMethod registers a custom RPC endpoint on the OpenClaw gateway. The method is callable from any gateway client (CLI, Control UI, or your own scripts) and runs in-process with the gateway.
Method signature:
handler receives a request object and a respond helper:
respond(success: boolean, data: unknown) sends the reply. Call it exactly once.
Naming convention. Use pluginId.action — for example voicecall.start, myplugin.status. Core gateway methods are reserved; attempting to register a name that conflicts with a core method is rejected with a diagnostic error at load time.
Calling from the CLI:
Register CLI commands
Register auto-reply commands
Plugins can register custom slash commands that execute without invoking the AI agent. This is useful for toggle commands, status checks, or quick actions that don’t need LLM processing.senderId: The sender’s ID (if available)channel: The channel where the command was sentisAuthorizedSender: Whether the sender is an authorized userargs: Arguments passed after the command (ifacceptsArgs: true)commandBody: The full command textconfig: The current OpenClaw config
name: Command name (without the leading/)description: Help text shown in command listsacceptsArgs: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won’t match and the message falls through to other handlersrequireAuth: Whether to require authorized sender (default: true)handler: Function that returns{ text: string }(can be async)
- Plugin commands are processed before built-in commands and the AI agent
- Commands are registered globally and work across all channels
- Command names are case-insensitive (
/MyStatusmatches/mystatus) - Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
- Reserved command names (like
help,status,reset, etc.) cannot be overridden by plugins - Duplicate command registration across plugins will fail with a diagnostic error
Register background services
Naming conventions
- Gateway methods:
pluginId.action(example:voicecall.status) - Tools:
snake_case(example:voice_call) - CLI commands: kebab or camel, but avoid clashing with core commands
Skills
Plugins can ship a skill in the repo (skills/<name>/SKILL.md).
Enable it with plugins.entries.<id>.enabled (or other config gates) and ensure
it’s present in your workspace/managed skills locations.
Distribution (npm)
Recommended packaging:- Main package:
wednesdayai(this repo;openclawremains a compatibility binary and SDK alias) - Plugins: separate public npm packages under the
@wednesdayaiorganization for new WednesdayAI packages (example:@wednesdayai/whatsapp). Existing inherited@openclaw/*packages remain compatibility installs only when the package is known to target this fork.
- Plugin
package.jsonshould includewednesdayai.extensionswith one or more built.jsentry files.openclaw.extensionsremains a legacy manifest-key fallback. - New scoped first-party plugin packages should set
publishConfig.accesstopublicor be published withnpm publish --access public. - Published plugins with runtime dependencies should ship built JavaScript entries; keep TypeScript source out of the npm runtime package.
wednesdayai plugins install <npm-spec>usesnpm pack, extracts into~/.openclaw/extensions/<id>/, and records the managed install. Activation remains explicit throughplugins.entries.<id>.enabled.- Config key stability: scoped packages are normalized to the unscoped id for
plugins.entries.*.
Example plugin: Voice Call
This repo includes a voice‑call plugin (Twilio or log fallback):- Source:
extensions/voice-call - Skill:
skills/voice-call - CLI:
openclaw voicecall start|status - Tool:
voice_call - RPC:
voicecall.start,voicecall.status - Config (twilio):
provider: "twilio"+twilio.accountSid/authToken/from(optionalstatusCallbackUrl,twimlUrl) - Config (dev):
provider: "log"(no network)
extensions/voice-call/README.md for setup and usage.
Safety notes
Plugins run in-process with the Gateway. Treat them as trusted code:- Only install plugins you trust.
- Prefer
plugins.allowallowlists. - Restart the Gateway after changes.
Testing plugins
Plugins can (and should) ship tests:- In-repo plugins can keep Vitest tests under
src/**(example:src/plugins/voice-call.plugin.test.ts). - Separately published plugins should run their own CI (lint/build/test) and validate
openclaw.extensionspoints at the built entrypoint (dist/index.js).
Troubleshooting
Plugin is not discovered at startup Check thatplugins.enabled is true (default) and that the plugin directory or file is reachable. Run openclaw plugins list to see what was discovered. If the plugin is absent, verify the path is listed under plugins.load.paths or the plugin file is in ~/.openclaw/extensions/. Non-bundled plugins without install provenance emit a startup warning — add the id to plugins.allow to suppress it and confirm trust.
Plugin loads but its config is rejected with an unknown-key error
Unknown fields inside plugins.entries.<id>.config are rejected by that
plugin’s JSON Schema. Ensure the config keys match the plugin manifest, and use
wednesdayai plugins info <id> to confirm the resolved id before debugging the
schema.
Gateway logs say plugin not found for a bundled plugin that still loads
Run wednesdayai plugins info <id> and wednesdayai plugins list --verbose. If the
plugin reports Origin: bundled and Status: loaded, the warning should not be
emitted for plugins.entries, plugins.allow, plugins.deny, or
plugins.slots.memory. If it persists, check for a stale plugins.installs.<id>
record whose sourcePath no longer matches the current bundled source. Avoid
adding bundled paths to plugins.load.paths; run wednesdayai plugins update <id>
or restart the gateway so bundled reconciliation can refresh the managed record.
In source checkouts, bundled plugins may declare built entrypoints that do not exist
until the package has been built. Discovery keeps package metadata as the primary
contract, but for bundled source-checkout plugins only it can fall back to known
source entrypoints inside the same extension directory. A missing built dist/
entry should not by itself require installing the bundled plugin into
plugins.load.paths.
Gateway method not callable after api.registerGatewayMethod
Method names must use pluginId.action form and must not conflict with core gateway methods. Conflicting names are rejected at load time with a diagnostic. Check gateway startup logs for registerGatewayMethod errors. Also confirm the plugin is enabled and the gateway restarted after the plugin change.
api.on hook not firing for a specific channel or trigger
Hook handlers receive all matching events — filtering by channelId or trigger is the handler’s responsibility. Add a console.log at the top of the handler to confirm it fires at all. Then gate on ctx.channelId or ctx.trigger as needed. For before_tool_call blocks, ensure priority is high enough that your handler runs before others that might short-circuit.