Create an OpenCode plugin for OpenWork. Scaffolds the plugin file with the correct API shape, tool definitions, and hook registration. Use when the user asks to 'create a plugin', 'write a plugin', or 'make a plugin that does X'.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Scaffold a working OpenCode plugin for use in OpenWork.
An OpenCode plugin is an async factory function that returns a hooks object.
Plugins can live in:
.opencode/plugins/my-plugin.ts (auto-discovered)~/.config/opencode/plugins/my-plugin.tsopencode.json plugin arrayfile: or https: path in the plugin arrayimport { z } from "zod";
export default async () => ({
tool: {
my_tool_name: {
description: "What this tool does.",
args: z.object({
input: z.string().describe("The input parameter."),
}).shape, // NOTE: .shape, not the ZodObject itself
async execute(args: { input: string }) {
// Your logic here. Can use fetch(), fs, child_process, etc.
return `Result: ${args.input}`;
},
},
},
});zodSchema.shape (a ZodRawShape), not the ZodObject.execute returns a string or { output: string; metadata?: Record<string, unknown> }.fetch() works — plugins run in-process inside the OpenCode runtime.process.env is accessible — use env vars for secrets/config.{
// Modify the system prompt
"experimental.chat.system.transform": async (input, output: { system: string[] }) => {
output.system.push("Extra instruction for the agent.");
},
// Define tools the agent can call
tool: {
tool_name: { description, args, execute },
},
// Run code before/after a tool executes
"tool.execute.before": async ({ tool, args }) => { /* ... */ },
"tool.execute.after": async ({ tool, args, result }) => { /* ... */ },
// React to lifecycle events
event: async ({ event }) => { /* ... */ },
}Add to opencode.json:
{
"plugin": [
".opencode/plugins/my-plugin.ts"
]
}Or install from npm:
{
"plugin": [
"my-published-plugin"
]
}OpenCode plugins are NOT the same as Anthropic's plugin format. Key differences:
| Aspect | OpenCode Plugin | Anthropic Plugin |
|---|---|---|
| Entry point | Async factory function | Manifest JSON |
| Tool args | Zod schema .shape | JSON Schema |
| Runtime | In-process (Bun/Node) | Sandboxed container |
| Auth | process.env | OAuth/API key in manifest |
| Distribution | npm / file path / URL | Anthropic marketplace |
To adapt an Anthropic plugin for OpenCode:
opencode.json provider config.import { z } from "zod";
export default async () => ({
"experimental.chat.system.transform": async (_input: unknown, output: { system: string[] }) => {
output.system.push("You have access to a note-taking system. Use save_note and list_notes.");
},
tool: {
save_note: {
description: "Save a note with a title and body.",
args: z.object({
title: z.string().describe("Note title"),
body: z.string().describe("Note content"),
}).shape,
async execute(args: { title: string; body: string }) {
const fs = await import("node:fs/promises");
const path = `.opencode/notes/${args.title.replace(/[^a-zA-Z0-9-_]/g, "_")}.md`;
await fs.mkdir(".opencode/notes", { recursive: true });
await fs.writeFile(path, `# ${args.title}\n\n${args.body}\n`);
return `Saved note: ${path}`;
},
},
list_notes: {
description: "List all saved notes.",
args: {},
async execute() {
const fs = await import("node:fs/promises");
try {
const files = await fs.readdir(".opencode/notes");
return files.filter(f => f.endsWith(".md")).join("\n") || "No notes yet.";
} catch {
return "No notes yet.";
}
},
},
},
});When the user describes what they want the plugin to do:
.opencode/plugins/<name>.ts with the plugin code.opencode.json plugin array if not already present.1345d51
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.