Plugins
Extend Arcana without forking core. Add custom tools, auth providers, provider hooks, and event listeners. Plugins let teams adapt Arcana to their specific workflows and policies.
Overview
Plugins are the advanced extension layer for Arcana. They let users and teams add custom behavior around tools, auth, providers, chat messages, permissions, and shell environment — all without modifying Arcana core.
Arcana must be useful without plugins. Plugins exist for custom cases:
- Custom provider authentication (OAuth, API keys)
- Additional tools and tool modifiers
- Chat message interception and transformation
- Permission overrides and policy enforcement
- Shell environment customization
- Provider model overrides
Commands
| Command | Description |
|---|---|
arcana plugin --help | Show plugin management options |
arcana plugin-store --help | Browse and install plugins from the store |
Plugin structure
A plugin is a JavaScript/TypeScript module that exports a default function. The function receives context and returns hooks.
Minimal plugin
// my-plugin/index.ts
export default async function myPlugin(input, options) {
return {
// Hook: called when a new message is received
async "chat.message"(msgInput, output) {
console.log("New message in session:", msgInput.sessionID);
},
};
}
Plugin module format
export type PluginModule = {
id?: string; // Optional plugin identifier
server: Plugin; // The plugin function (required)
tui?: never; // Reserved for future TUI plugins
};
export type Plugin = (
input: PluginInput,
options?: PluginOptions
) => Promise<Hooks>;
Plugin input
When Arcana loads your plugin, it provides:
| Field | Type | Description |
|---|---|---|
client | SDK client | HTTP client for the Arcana server API |
project | Project | Current project info (VCS, path, etc.) |
directory | string | Current working directory |
worktree | string | Active worktree path |
serverUrl | URL | Local server URL |
$ | BunShell | Shell helper for running commands |
Available hooks
Plugins return an object with hook functions. Each hook is called at a specific point in Arcana's lifecycle.
chat.message
Called when a new user message is received. Use this to intercept, annotate, or transform messages.
async "chat.message"(input, output) {
// input: { sessionID, agent?, model?, messageID?, variant? }
// output: { message, parts }
// Modify output.message or output.parts to transform the message
}
chat.params
Modify parameters sent to the LLM (temperature, topP, topK, maxOutputTokens).
async "chat.params"(input, output) {
// output: { temperature, topP, topK, maxOutputTokens, options }
output.temperature = 0.7;
}
chat.headers
Add custom HTTP headers to LLM API requests.
async "chat.headers"(input, output) {
// output: { headers }
output.headers["X-Custom-Header"] = "value";
}
tool.execute.before
Intercept tool calls before execution. Modify arguments or block the call.
async "tool.execute.before"(input, output) {
// input: { tool, sessionID, callID }
// output: { args }
if (input.tool === "shell" && output.args.command?.includes("rm -rf")) {
throw new Error("Blocked: dangerous command");
}
}
tool.execute.after
Process tool results after execution.
async "tool.execute.after"(input, output) {
// input: { tool, sessionID, callID, args }
// output: { title, output, metadata }
if (input.tool === "shell") {
output.metadata = { ...output.metadata, logged: true };
}
}
tool.definition
Modify tool descriptions and parameters sent to the LLM.
async "tool.definition"(input, output) {
// input: { toolID }
// output: { description, parameters }
if (input.toolID === "shell") {
output.description = "Run shell commands (restricted)";
}
}
permission.ask
Override permission decisions for tool calls.
async "permission.ask"(input, output) {
// input: Permission object
// output: { status: "ask" | "deny" | "allow" }
if (input.type === "shell" && input.command?.startsWith("git")) {
output.status = "allow"; // Auto-allow git commands
}
}
shell.env
Inject environment variables into the shell for tool execution.
async "shell.env"(input, output) {
// input: { cwd, sessionID?, callID? }
// output: { env }
output.env.NODE_ENV = "test";
output.env.CUSTOM_VAR = "value";
}
command.execute.before
Intercept slash commands before execution.
async "command.execute.before"(input, output) {
// input: { command, sessionID, arguments }
// output: { parts }
if (input.command === "/mycommand") {
output.parts = [{ type: "text", text: "Custom response" }];
}
}
event
Listen to all Arcana events (session, config, provider changes).
async event({ event }) {
if (event.type === "session.created") {
console.log("New session:", event.properties);
}
}
config
Modify the configuration after it's loaded.
async config(config) {
// Modify config before Arcana uses it
config.model = "anthropic/claude-sonnet-4";
}
dispose
Clean up resources when the plugin is unloaded.
async dispose() {
await this.client.close();
}
Auth provider hooks
Plugins can register custom authentication methods for providers. This is how Arcana supports providers like GitHub Copilot, Google Vertex, and enterprise SSO.
OAuth authentication
export default async function myPlugin(input) {
return {
auth: {
provider: "my-provider",
methods: [
{
type: "oauth",
label: "Sign in with MyProvider",
async authorize(inputs) {
// Start OAuth flow
return {
url: "https://auth.example.com/authorize?...",
instructions: "Complete sign-in in your browser",
method: "auto",
async callback() {
// Exchange tokens
return {
type: "success",
access: "access_token",
refresh: "refresh_token",
expires: Date.now() / 1000 + 3600,
};
},
};
},
},
],
},
};
}
API key authentication
export default async function myPlugin(input) {
return {
auth: {
provider: "my-provider",
methods: [
{
type: "api",
label: "Enter API key",
prompts: [
{
type: "text",
key: "apiKey",
message: "Enter your API key",
validate: (v) => (v.length > 0 ? undefined : "Required"),
},
],
async authorize(inputs) {
// Validate the key
return { type: "success", key: inputs.apiKey };
},
},
],
},
};
}
Provider hooks
Plugins can register custom providers or override model lists for existing providers.
export default async function myPlugin(input) {
return {
provider: {
id: "my-custom-provider",
async models(provider, ctx) {
return {
"my-model-1": { name: "My Model 1", contextWindow: 128000 },
"my-model-2": { name: "My Model 2", contextWindow: 32000 },
};
},
},
};
}
Custom tools
Plugins can register new tools that the agent can call during sessions.
import { defineTool } from "@arcana/plugin";
const myTool = defineTool({
name: "my-custom-tool",
description: "Does something useful",
parameters: {
type: "object",
properties: {
input: { type: "string", description: "Input text" },
},
required: ["input"],
},
async execute(args, ctx) {
return { result: `Processed: ${args.input}` };
},
});
export default async function myPlugin() {
return {
tool: {
"my-custom-tool": myTool,
},
};
}
Configuration
Plugins are configured in ~/.arcana/config.json or arcana.json:
Plain plugin (no options)
{
"plugin": [
"my-plugin"
]
}
Plugin with options
{
"plugin": [
["my-plugin", { "verbose": true, "apiKey": "..." }]
]
}
Local plugin (from file)
{
"plugin": [
"./path/to/my-plugin/index.ts"
]
}
Workspace trust
Project-local plugins require workspace trust before they are loaded. This prevents untrusted code from executing automatically.
# Trust the current workspace
arcana trust
# Check trust status
arcana trust status
See MCP & Tools: Workspace trust for full details.
Examples
Rate limiter plugin
export default async function rateLimiter() {
const counts = new Map<string, number>();
return {
async "tool.execute.before"(input, output) {
const key = input.tool;
const count = (counts.get(key) ?? 0) + 1;
counts.set(key, count);
if (count > 10) {
throw new Error(`Rate limit exceeded for tool: ${key}`);
}
},
};
}
Audit logger plugin
import { appendFile } from "fs/promises";
export default async function auditLogger() {
const logFile = "/tmp/arcana-audit.log";
return {
async "tool.execute.after"(input, output) {
const entry = JSON.stringify({
timestamp: new Date().toISOString(),
tool: input.tool,
session: input.sessionID,
args: input.args,
});
await appendFile(logFile, entry + "\n");
},
};
}
Safety model
Plugins operate within Arcana's permission and mode system:
| Plugin output | Observe | Advise | Ask | Enforce | Locked |
|---|---|---|---|---|---|
| Annotation | Record | Show | Show | Show | Show if approved |
| Warning | Record | Show | Show | Show | Show if approved |
| Block recommendation | Record | Warn | Ask | Block if policy matches | Block unless allowlisted |
access secrets, send network requests, modify source files, approve their own blocked actions, change the active mode, or hide decisions from the capsule.
How plugins work
- Plugins are loaded at session start from the config
pluginarray. - Each plugin receives context (client, project, directory) and returns hooks.
- Hooks are called at specific lifecycle points (message, tool, permission, etc.).
- Plugins can modify behavior but cannot override Arcana's core safety model.
- Project-local plugins require workspace trust; global plugins are always available.
- Multiple plugins can be active simultaneously — hooks are called in order.