Features

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.

Updated · v0.3.15

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:

Commands

CommandDescription
arcana plugin --helpShow plugin management options
arcana plugin-store --helpBrowse 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:

FieldTypeDescription
clientSDK clientHTTP client for the Arcana server API
projectProjectCurrent project info (VCS, path, etc.)
directorystringCurrent working directory
worktreestringActive worktree path
serverUrlURLLocal server URL
$BunShellShell 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 outputObserveAdviseAskEnforceLocked
AnnotationRecordShowShowShowShow if approved
WarningRecordShowShowShowShow if approved
Block recommendationRecordWarnAskBlock if policy matchesBlock unless allowlisted
Plugins must not silently

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