Skip to content

Plugins & Tools

Claudius plugins are small middleware that run around the chat message lifecycle. The same three lifecycle hooks exist on the client (the React widget) and the server (the Worker), so you can inject context, redact PII, route to different models, log analytics events, or answer locally — without forking the widget.

HookRunsCan
onBeforeSendbefore a message is sentmodify it, replace it, answer locally (respondWith), or cancel it (abort)
onAfterReceiveafter the reply arrivesmodify or replace the reply
onErrorwhen a send failsobserve the error; on the client, recover with a fallback reply

All hooks may be async. A hook that throws is caught and logged, so a single misbehaving plugin will not break the chat — write security-sensitive transforms (like redaction) defensively.

Pass an array of plugins to ChatWidget. Hooks run in array order.

import { ChatWidget, pluginRedactPII, pluginAnalytics } from "claudius-chat-widget";
export function App() {
return (
<ChatWidget
apiUrl="https://your-worker.workers.dev"
plugins={[
pluginRedactPII(),
pluginAnalytics({ onEvent: (e) => console.log(e) }),
]}
/>
);
}

onBeforeSend returns the (possibly modified) message — and the returned message is what is both displayed and sent, so a redaction is visible to the user.

The onBeforeSend context can answer without a network call, or cancel the send entirely:

import type { ClaudiusPlugin } from "claudius-chat-widget";
const slashCommands: ClaudiusPlugin = {
name: "slash-commands",
onBeforeSend(message, ctx) {
if (message.content === "/clear") return ctx.abort();
if (message.content.startsWith("/help")) {
ctx.respondWith("Type a question and press Enter.");
}
},
};
  • ctx.respondWith(reply) — render reply as the assistant message and skip the API. reply is a string or { content, sources }.
  • ctx.abort() — drop the message and render nothing.

onError can recover the same way, replacing the error UI with a reply:

const fallback: ClaudiusPlugin = {
name: "fallback",
onError: (_err, ctx) =>
ctx.respondWith("We're offline right now — email help@example.com."),
};

Three plugins ship with claudius-chat-widget.

pluginAnalytics — emit a structured event for every sent message, reply, and error:

import { pluginAnalytics } from "claudius-chat-widget";
pluginAnalytics({
onEvent: (event) => window.gtag?.("event", event.type, event),
includeContent: false, // record only character counts, not message text
});

pluginRedactPII — strip emails, phone numbers, SSNs, and card-like numbers before the message leaves the browser:

import { pluginRedactPII } from "claudius-chat-widget";
pluginRedactPII(); // sensible defaults
pluginRedactPII({ replacement: "***", redactReplies: true });

pluginCannedResponses — answer matched intents locally, with no API call:

import { pluginCannedResponses } from "claudius-chat-widget";
pluginCannedResponses({
rules: [
{ match: "hours", reply: "We're open 9–5, Mon–Fri." },
{ match: /pricing|cost/i, reply: "See https://example.com/pricing." },
],
});

A plugin is an object with a name and any of the three hooks:

import type { ClaudiusPlugin } from "claudius-chat-widget";
const pageContext: ClaudiusPlugin = {
name: "page-context",
onBeforeSend(message) {
return { ...message, content: `[on ${location.pathname}] ${message.content}` };
},
};

The Worker exposes the equivalent hooks as Hono middleware. Server hooks operate on the whole request (the messages array) and the reply string, rather than on a single widget message.

Register plugins in worker/src/index.ts — the file ships this block with an empty list, so just drop your plugins in:

import { chatPlugins, pluginRedactPII } from "./plugins";
const serverPlugins = [pluginRedactPII({ redactReplies: true })];
if (serverPlugins.length > 0) {
app.use("/api/chat", chatPlugins(serverPlugins));
}
import type { ClaudiusServerPlugin } from "./plugins";
const example: ClaudiusServerPlugin = {
name: "example",
onBeforeSend(messages, ctx) {
// Inspect ctx.env, transform messages, or short-circuit the model:
// ctx.respondWith("a canned reply");
return messages;
},
onAfterReceive(reply) {
return reply.trim();
},
onError(error) {
console.error("chat failed:", error.message);
},
};
  • onBeforeSend(messages, ctx) — return a new messages array, or call ctx.respondWith(text) to answer without calling Claude.
  • onAfterReceive(reply, ctx) — return a new reply string.
  • onError(error, ctx) — observe failures.

The same three plugins, adapted for the request lifecycle:

import {
chatPlugins,
pluginAnalytics,
pluginRedactPII,
pluginCannedResponses,
} from "./plugins";
const serverPlugins = [
pluginRedactPII(), // redact every message before it reaches Claude
pluginCannedResponses({
rules: [{ match: /refund/i, reply: "See our refund policy at /refunds." }],
}),
pluginAnalytics({ onEvent: (e) => console.log(e) }),
];
app.use("/api/chat", chatPlugins(serverPlugins));

Redacting server-side is defense in depth — it runs even for clients that don’t ship the widget redactor. A canned (short-circuit) response skips the model, and with it the built-in D1 analytics for that turn.

  • Hooks run in array order; the first respondWith / abort wins and stops that hook’s chain.
  • A throwing hook is caught and logged — it never breaks the chat.
  • The client and server interfaces are parallel but not identical: a widget message carries an id and sources; the server works on plain { role, content } messages.

These complement plugins and cover most customization without code:

Extension pointWhat it controls
System prompt (worker/src/system-prompt.ts)The bot’s personality, knowledge, guardrails, and FAQ answers
TranslationsEvery user-facing UI string
Theming and widget/tailwind.config.tsColors, fonts, radii
Proactive triggersWhen the widget opens or greets proactively
Worker env vars (CLAUDE_MODEL, MAX_TOKENS, rate limits)Model behavior and abuse protection
  • Anthropic tool-use / function calling with a declarative tool registry, so the bot can call your APIs mid-conversation — #51
  • Plugin SDK RFC for Claudius v2 — the longer-term plugin architecture — #79