agentsclimarketplace

Opencode plugin development

Skill gdm257/cc-plugins/plugins/agent-skills/skills/opencode-plugin-development

This skill should be used when the user asks to "create an OpenCode plugin", "develop an OpenCode plugin", "write an OpenCode plugin", "build an OpenCode plugin", "make an OpenCode plugin", "add a hook to OpenCode", "subscribe to OpenCode events", "create a custom tool for OpenCode", or needs guidance on OpenCode plugin architecture, events, custom tools, TypeScript support, or dependencies management.From its SKILL.md

Install
npx -y skills add gdm257/cc-plugins --skill opencode-plugin-development

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

19.4 KB, ~3.6k tokens by cl100k_base, as published. Nobody here has run it

OpenCode Plugin Development Guide

Create plugins to extend OpenCode's functionality by hooking into events, adding custom tools, and modifying behavior.

Overview

OpenCode plugins are JavaScript/TypeScript modules that export a plugin function. The function receives a context object and returns a hooks object defining event hooks and custom tools.

Plugin Types

OpenCode supports two plugin installation methods:

Config Plugin Field (Recommended)

Define plugins in your opencode.json plugin array:

{
  "plugin": ["[email protected]", "@scope/enterprise-plugin"]
}

Characteristics:

  • Plugins are npm packages installed via Bun
  • Dependencies declared in package.json are automatically installed
  • Published to npm registry for distribution
  • Version management via npm versioning
  • Suitable for: distributable plugins, plugins with external dependencies

Local Plugin Directory

Place plugin files directly in .opencode/plugins/:

.opencode/plugins/
└── my-plugin.ts

Characteristics:

  • Local files loaded via file:// URLs
  • No dependency installation - all dependencies must be pre-installed
  • Quick iteration for development
  • Suitable for: private plugins, prototypes, plugins without external dependencies

Comparison:

FeatureConfig plugin FieldLocal .opencode/plugins/
Dependency Installation✅ Automatic via Bun❌ Manual
Distributionnpm packageLocal files
Version Controlnpm versioningFile-based
Use CaseProduction pluginsDevelopment/Private

Recommendation: Use the config plugin field for most plugins.

Dependencies

Local plugins in .opencode/plugins/ cannot import external npm packages.

Plugins run within the OpenCode server process, which means:

  • Available imports:

    • Node.js/Bun built-in modules (fs, path, http, etc.)
    • @opencode-ai/plugin SDK (official API)
    • Any packages already installed in the OpenCode server environment
  • Cannot import:

    • Arbitrary npm packages not pre-installed in the server
    • Dependencies from your project's node_modules

Example - Only use built-ins and official SDK:

import type { Plugin } from "@opencode-ai/plugin"
import { readFileSync } from "fs" // ✅ Built-in module
import { client } from "./local" // ✅ Relative import

export const MyPlugin: Plugin = async () => {
  return {
    // Return hooks
  }
}

To use npm dependencies, publish as an npm package:

  1. Create an npm package with your plugin
  2. Publish to npm registry
  3. Reference it in your config:
{
  "plugin": ["[email protected]"]
}

Dependencies declared in the package's package.json will be automatically installed via Bun.

TypeScript Support

Import types from @opencode-ai/plugin:

import type { Plugin } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async (ctx) => {
  // ctx is fully typed
  const { project, client, $, directory, worktree } = ctx

  return {
    // Return hooks
  }
}

Context Properties

The plugin function receives:

PropertyTypeDescription
projectobjectCurrent project information
clientSDK clientInteract with the AI assistant
$Bun shellExecute shell commands
directorystringCurrent working directory
worktreestringGit worktree path
import type { Plugin } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
  return {
    // Return hooks
  }
}

Hooks Reference

Plugins extend OpenCode through hooks - callback functions that run at specific times. A plugin provides hooks to react to events, register custom tools, or modify global configuration.

Complete plugin structure with all available hooks:

import type { Plugin } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async (input) => {
  return {
    // === Event Hooks (called each time the event occurs) ===
    // Subscribe to all events (session.*, file.*, message.*, etc.)
    event: async ({ event }) => {
      switch (event.type) {
        case "session.idle":
          break
        case "file.edited":
          break
        // ... other events
      }
    },
    // Specific event hooks with custom input/output
    "chat.message": async ({ sessionID, agent, model, messageID, variant }, { message, parts }) => {},
    "chat.params": async ({ sessionID, agent, model, provider, message }, { temperature, topP, topK, options }) => {},
    "chat.headers": async ({ sessionID, agent, model, provider, message }, { headers }) => {},
    "permission.ask": async (input, { status }) => {},
    "command.execute.before": async ({ command, sessionID, arguments }, { parts }) => {},
    "tool.execute.before": async ({ tool, sessionID, callID }, { args }) => {},
    "tool.execute.after": async ({ tool, sessionID, callID }, { title, output, metadata }) => {},
    // Experimental event hooks
    "experimental.chat.messages.transform": async (_, { messages }) => {},
    "experimental.chat.system.transform": async ({ sessionID, model }, { system }) => {},
    "experimental.session.compacting": async ({ sessionID }, { context, prompt }) => {},
    "experimental.text.complete": async ({ sessionID, messageID, partID }, { text }) => {},

    // === Registration Hooks (called once at initialization) ===
    tool: {
      [name: string]: {
        description: string,
        args: { [key: string]: z.ZodType },
        execute: async (args, context) => string,
      },
    },
    auth: {
      provider: string,
      loader: async (getAuth, provider) => Record<string, any>,
      methods: Array<{ type: "oauth" | "api", label: string, authorize: async (inputs?) => {...} }>,
    },
    config: async (config) => {},
  }
}

Hooks Summary

HookDescriptionWhen
Event Hooks (per-event)
eventCatch all events (session., file., message.*, etc.) via event.typeeach
chat.messageReact to new messageseach
chat.paramsModify LLM paramseach
chat.headersModify LLM headerseach
permission.askHandle permissionseach
command.execute.beforeBefore commandeach
tool.execute.beforeBefore tooleach
tool.execute.afterAfter tooleach
Experimental Event Hooks (per-event)
experimental.chat.messages.transformTransform messageseach
experimental.chat.system.transformTransform systemeach
experimental.session.compactingSession compactioneach
experimental.text.completeText completioneach
Registration Hooks (once at init)
toolRegister toolsinit
authRegister authinit
configModify configinit

All events available via event hook:

CategoryEvents
Sessionsession.created, session.compacted, session.deleted, session.diff, session.error, session.idle, session.status, session.updated
Filefile.edited, file.watcher.updated
Messagemessage.updated, message.removed, message.part.updated, message.part.removed
Commandcommand.executed
Permissionpermission.asked, permission.replied
LSPlsp.client.diagnostics, lsp.updated
Todotodo.updated
Shellshell.env
Serverserver.connected, server.instance.disposed
Installationinstallation.updated
TUItui.prompt.append, tui.command.execute, tui.toast.show, tui.session.select

Custom Tools

Add custom tools using the tool helper:

import { tool } from "@opencode-ai/plugin"

export const CustomToolsPlugin: Plugin = async () => {
  return {
    tool: {
      greet: tool({
        description: "Greet a user by name",
        args: {
          name: tool.schema.string(),
        },
        async execute(args) {
          return `Hello, ${args.name}!`
        },
      }),
      calculate: tool({
        description: "Perform basic calculations",
        args: {
          a: tool.schema.number(),
          b: tool.schema.number(),
          operation: tool.schema.enum(["add", "subtract", "multiply", "divide"]),
        },
        async execute(args) {
          switch (args.operation) {
            case "add":
              return args.a + args.b
            case "subtract":
              return args.a - args.b
            case "multiply":
              return args.a * args.b
            case "divide":
              return args.a / args.b
          }
        },
      }),
    },
  }
}

Slash Commands

Register slash commands and handle their execution using the config callback and command.execute.before event.

Command Registration

Use the config callback to register slash commands in OpenCode's configuration:

import type { Plugin } from "@opencode-ai/plugin"

export const SlashCommandPlugin: Plugin = async () => {
  return {
    config: async (cfg) => {
      cfg.command ??= {}
      cfg.command["myplugin"] = {
        template: "",
        description: "Execute my plugin commands",
      }
    },
  }
}

Result:

  • Command /myplugin appears in autocomplete when user types /
  • Description shows in command help UI

Command Handling

Intercept and process command execution via command.execute.before:

export const SlashCommandPlugin: Plugin = async () => {
    return {
        "command.execute.before": async (input, _output) => {
            if (input.command === "myplugin") {
                const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
                const subcommand = args[0]?.toLowerCase() || ""

                if (subcommand === "status") {
                    // Handle /myplugin status
                    console.log("Status command executed")
                } else if (subcommand === "config") {
                    // Handle /myplugin config
                    console.log("Config command executed")
                } else {
                    // Handle unknown subcommand - show help
                    console.log("Available: status, config")

Config Modifications

The config callback receives the full OpenCode configuration object, allowing you to modify settings beyond just commands:

import type { Plugin } from "@opencode-ai/plugin"

export const ConfigPlugin: Plugin = async () => {
  return {
    config: async (cfg) => {
      // Model configuration
      cfg.model = "anthropic/claude-sonnet-4-20250506"
      cfg.small_model = "anthropic/haiku-4-20250506"
      cfg.default_agent = "build"

      // Agent configuration
      cfg.agent ??= {}
      cfg.agent.build ??= {}
      cfg.agent.build.model = "anthropic/claude-sonnet-4-20250506"
      cfg.agent.build.temperature = 0.5
      cfg.agent.build.prompt = "You are a careful developer."

      // Experimental features
      cfg.experimental ??= {}
      cfg.experimental.batch_tool = true
      cfg.experimental.primary_tools = ["read", "bash", "edit"]
      cfg.experimental.continue_loop_on_deny = true

      // Provider configuration
      cfg.provider ??= {}
      cfg.provider.anthropic ??= {}
      cfg.provider.anthropic.options ??= {}
      cfg.provider.anthropic.options.timeout = 60000

      // Permission configuration
      cfg.permission ??= {}
      cfg.permission.read = "allow"
      cfg.permission.edit = "ask"

      // MCP servers
      cfg.mcp ??= {}
      cfg.mcp["pokemon-server"] = {
        type: "local",
        command: ["uv", "--directory", "/path/to/server", "run", "server"],
        enabled: true,
      }

      // Server settings
      cfg.server ??= {}
      cfg.server.port = 4096
      cfg.server.hostname = "0.0.0.0"
      cfg.server.mdns = true

      // UI settings
      cfg.username = "my-user"
      cfg.share = "manual"

      // Compaction settings
      cfg.compaction ??= {}
      cfg.compaction.auto = true
      cfg.compaction.prune = true

      // Workspace backend
      cfg.workspaceBackend = "git"
    },
  }
}

Available configuration fields:

FieldTypeDescription
modelstringDefault model (format: provider/model)
small_modelstringSmall model for auxiliary tasks
default_agentstringDefault agent name
agentobjectAgent configurations (build, plan, general, explore, etc.)
providerobjectCustom provider and model overrides
commandobjectSlash commands registration
mcpobjectMCP server configurations
experimentalobjectExperimental feature flags
permissionobjectTool permission policies
serverobjectServer settings (port, hostname, mdns, cors)
usernamestringDisplay name
share"manual" | "auto" | "disabled"Sharing behavior
compactionobjectSession compaction settings
workspaceBackend"git"Workspace backend

Auth Hook

Register custom authentication providers:

import type { Plugin } from "@opencode-ai/plugin"

export const AuthPlugin: Plugin = async () => {
  return {
    auth: {
      provider: "my-provider",
      methods: [
        {
          type: "oauth",
          label: "Sign in with MyProvider",
          prompts: [{ type: "text", key: "tenant", message: "Enter tenant ID" }],
          authorize: async (inputs) => {
            const response = await fetch("https://my-provider.com/oauth/token", {
              method: "POST",
              body: JSON.stringify({
                client_id: inputs.clientId,
                client_secret: inputs.clientSecret,
                tenant: inputs.tenant,
              }),
            })
            const data = await response.json()
            return {
              type: "success",
              provider: "my-provider",
              refresh: data.refresh_token,
              access: data.access_token,
              expires: data.expires_in,
            }
          },
        },
      ],
    },
  }
}

Logging

Use structured logging via client.app.log():

await client.app.log({
  body: {
    service: "my-plugin",
    level: "debug" | "info" | "warn" | "error",
    message: "Log message",
    extra: {
      // Additional context
      foo: "bar",
    },
  },
})

Additional Resources

Detailed Reference

  • references/events.md - Complete event reference with full type signatures

Example Plugins

Complete, runnable plugin examples in examples/:

FileDemonstrates
notification.tsSession events (session.created, session.idle, etc.)
env-protection.tstool.execute.before, file.edited hooks
inject-env.tsshell.env hook, multiple plugin patterns
compaction.tsexperimental.session.compacting variations
custom-tools.tsCustom tool registration with tool helper

What ships with it: 6 files

25.0 KB alongside SKILL.md, 5 of them executable

examples/

references/

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.