writing-opencode-plugins

Guides development of OpenCode plugins including project structure, testing patterns, and publishing. Use when creating or modifying OpenCode plugins.

16 stars

Best use case

writing-opencode-plugins is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Guides development of OpenCode plugins including project structure, testing patterns, and publishing. Use when creating or modifying OpenCode plugins.

Teams using writing-opencode-plugins should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/writing-opencode-plugins/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/tools/writing-opencode-plugins/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/writing-opencode-plugins/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How writing-opencode-plugins Compares

Feature / Agentwriting-opencode-pluginsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Guides development of OpenCode plugins including project structure, testing patterns, and publishing. Use when creating or modifying OpenCode plugins.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Writing OpenCode Plugins

## Project structure

Use the shim pattern to separate development from plugin loading:

```
plugin-name/
├── src/
│   └── main.ts              # Main implementation
├── .opencode/
│   ├── package.json         # Plugin dependencies
│   └── plugins/
│       └── shim.ts         # Loads plugin from src/
├── test/
│   ├── unit.test.ts        # Unit tests
│   └── e2e.test.ts        # E2E tests
├── package.json            # Root package.json
└── tsconfig.json           # TypeScript config
```

### Shim file (.opencode/plugins/*.ts)

The shim file is loaded by OpenCode and re-exports from src:

```ts
export { PluginName as PluginNamePlugin } from "../../src/main"
```

Benefits:
- ✅ Development with TypeScript and type safety
- ✅ Easy unit/integration testing
- ✅ Source code not duplicated in .opencode/

## Plugin structure

A plugin exports a function that receives context and returns hooks:

```ts
export const MyPlugin: Plugin = async ({ directory, worktree, project, client, $ }) => {
  return {
    "shell.env": async (input, output) => {
      // input.cwd - current working directory
      // output.env - modify shell environment
    },
  }
}
```

### Context parameters

- `directory` - Current working directory
- `worktree` - Git worktree path
- `project` - Project information
- `client` - OpenCode SDK client
- `$` - Bun shell API

### Available hooks

Common hooks:
- `shell.env` - Inject environment variables before shell execution
- `tool.execute.before` - Intercept tool calls before execution
- `tool.execute.after` - Post-process tool results
- `file.edited` - React to file changes

## Testing strategy

### Unit tests

Test core logic in isolation using `bun test`:

```ts
// test/unit.test.ts
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
import { coreFunction } from "../src/main"
import { tmpdir } from "os"
import { join } from "path"
import { mkdir, writeFile, rm } from "fs/promises"

describe("coreFunction", () => {
  let testDir: string

  beforeEach(async () => {
    testDir = join(tmpdir(), `test-${Date.now()}`)
    await mkdir(testDir, { recursive: true })
  })

  afterEach(async () => {
    await rm(testDir, { recursive: true, force: true })
  })

  test("handles basic case", async () => {
    const result = await coreFunction(testDir)
    expect(result).toBeDefined()
  })
})
```

### E2E tests

Test full plugin integration with OpenCode CLI:

```ts
// test/e2e.test.ts
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
import { $ } from "bun"

describe("E2E", () => {
  test("plugin works with OpenCode CLI", async () => {
    try {
      const result = await $`opencode run 'test command'`.quiet()
      expect(result.exitCode).toBe(0)
      expect(result.stdout).toContain("expected")
    } catch (error) {
      if (error instanceof Error && error.message.includes("ENOENT")) {
        console.log("Skipping: opencode CLI not found")
        return
      }
      throw error
    }
  }, { timeout: 30_000 })
})
```

### Test fixtures

Committed test data in `test/fixtures/`:

```
test/fixtures/
├── basic/           # Basic scenarios
├── advanced/        # Complex scenarios
└── e2e/           # E2E test workspace
```

Test fixtures can contain safe-to-commit test data, encryption keys, etc.

## Dependencies

### Root package.json

Contains dev dependencies and publishing info:

```json
{
  "name": "@scope/plugin-name",
  "version": "0.1.0",
  "main": "./src/main.ts",
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/main.ts",
      "import": "./src/main.ts"
    }
  },
  "scripts": {
    "test": "bun test",
    "test:unit": "bun test test/unit.test.ts",
    "test:e2e": "bun test test/e2e.test.ts"
  },
  "dependencies": {
    "@opencode-ai/plugin": "1.2.10"
  },
  "publishConfig": {
    "access": "public"
  }
}
```

### .opencode/package.json

Plugin runtime dependencies only:

```json
{
  "name": "plugin-name-runtime",
  "private": true,
  "dependencies": {
    "@opencode-ai/plugin": "1.2.10"
  }
}
```

OpenCode automatically runs `bun install` in `.opencode/` at startup.

## Publishing

### Using np (recommended)

```bash
# Install dependencies
bun install

# Login to npm
npm login

# Publish with version prompt
bunx np --any-branch
```

### Manual publish

```bash
# Bump version
npm version patch  # or minor, or major

# Publish to npm
npm publish --access public
```

### .npmignore

Exclude development files from npm package:

```
# Testing
test/
coverage/

# OpenCode plugin (local use)
.opencode/
.opencode.json

# Development notes
notes/
AGENTS.md

# Serena
.serena/

# GitHub CI
.github/

# Build/cache
node_modules/
dist/
out/
*.tsbuildinfo
.cache/
.eslintcache

# Logs
logs/
*.log

# Environment files
.env
.env.*
.env.keys
.env.*.keys

# macOS
.DS_Store

# IDE
.idea/
*.swp
*.swo

# npm
bun.lock
```

## TypeScript configuration

Use strict TypeScript with Bun defaults:

```json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false
  }
}
```

## AGENTS.md (Bun instructions)

Include Bun-specific guidance in AGENTS.md:

```markdown
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---

Default to using Bun instead of Node.js.

- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun install` instead of `npm install`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
```

## Development workflow

1. Edit `src/main.ts` - main implementation
2. Run tests: `bun test`
3. Shim in `.opencode/plugins/*.ts` auto-reloads on OpenCode restart

## Plugin installation

### Project-level (development)

Plugin files in `.opencode/plugins/` auto-load on OpenCode restart.

### Global (all projects)

```bash
mkdir -p ~/.config/opencode/plugins
cp .opencode/plugins/*.ts ~/.config/opencode/plugins/
cp .opencode/package.json ~/.config/opencode/package.json
```

### From npm (users)

Add to `opencode.json`:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@scope/plugin-name"]
}
```

OpenCode auto-installs npm packages to `~/.cache/opencode/node_modules/`.

## Best practices

### Extract testable functions

Separate core logic from hooks:

```ts
// Bad: Logic inside hook
export const Plugin = async () => ({
  "shell.env": async (input, output) => {
    // All logic here...
  }
})

// Good: Extracted function
async function coreLogic(path: string): Promise<Record<string, string>> {
  // Pure logic, easy to test
}

export const Plugin = async () => ({
  "shell.env": async (input, output) => {
    const result = await coreLogic(input.cwd)
    Object.assign(output.env, result)
  }
})
```

### Error handling

Use try/catch with meaningful errors:

```ts
try {
  const result = await riskyOperation()
  return result
} catch (error) {
  console.error("Operation failed:", error)
  throw new Error(`Failed to process: ${error}`)
}
```

### Async hooks

All hooks are async - use async/await:

```ts
export const Plugin = async () => ({
  "shell.env": async (input, output) => {
    const data = await loadData()
    output.env.VAR = data.value
  }
})
```

### Non-mutating output

Check before setting:

```ts
if (!(key in output.env)) {
  output.env[key] = value
}
```

### Quiet mode

Suppress console output for clean logs:

```ts
someLibrary.configure({ quiet: true })
```

## CI/CD

Example GitHub Actions:

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - run: bun install
      - run: bun test
```

## References

- [OpenCode Plugins Documentation](https://opencode.ai/docs/plugins/)
- [OpenCode SDK Documentation](https://opencode.ai/docs/sdk/)
- [Bun Documentation](https://bun.sh/docs)

Related Skills

pytest-plugins

16
from diegosouzapw/awesome-omni-skill

Use when pytest plugin ecosystem including pytest-cov, pytest-mock, and custom plugin development.

opencode

16
from diegosouzapw/awesome-omni-skill

OpenCode - Open source AI coding agent for terminal, desktop, and IDE with multi-provider LLM support, custom agents, MCP integration, and granular permissions

opencode-docs

16
from diegosouzapw/awesome-omni-skill

OpenCode + OhMyOpenCode configuration reference - agents, tools, MCP, skills, commands, hooks, permissions, providers, and Sisyphus orchestration patterns

opencode-config

16
from diegosouzapw/awesome-omni-skill

Use when configuring OpenCode CLI - changing default model, adding providers, setting baseURL, or troubleshooting model selection issues

moai-tool-opencode

16
from diegosouzapw/awesome-omni-skill

OpenCode.ai open-source AI coding agent comprehensive reference. Use when working with OpenCode TUI, CLI, IDE integration, configuring agents, tools, MCP servers, creating plugins, or developing with the SDK.

developing-claude-code-plugins

16
from diegosouzapw/awesome-omni-skill

Build, test, and distribute Claude Code plugins with slash commands, agents, skills, hooks, MCP servers, and LSP servers. MUST be loaded when creating, reviewing, debugging, or distributing plugins. Use PROACTIVELY when user mentions plugins, extensions, marketplaces, or wants to add custom commands/agents to Claude Code.

academic-writing-cs

16
from diegosouzapw/awesome-omni-skill

Comprehensive toolkit for writing high-quality computer science research papers (conference, journal, thesis). Provides narrative construction guidance, sentence-level clarity principles (Gopen & Swan), academic phrasebank, CS-specific conventions, and section-by-section quality checklists. Use when assisting with academic paper writing, revision, or structure planning across all stages from drafting to submission.

acceptance-test-writing

16
from diegosouzapw/awesome-omni-skill

Guide for writing high-quality acceptance criteria and acceptance tests using industry-standard BDD (Behavior-Driven Development) and ATDD (Acceptance Test-Driven Development) practices. Use this skill when creating acceptance criteria for user stories, writing Gherkin scenarios, or implementing acceptance test specifications following Given-When-Then format.

writing-plans

16
from diegosouzapw/awesome-omni-skill

Use when you have a spec or requirements for a multi-step task, before touching code

writing-documentation

16
from diegosouzapw/awesome-omni-skill

Creates technical documentation including READMEs, API references, user guides, architecture docs, ADRs, and runbooks. Use for requests to create, write, generate, or draft documentation. Trigger phrases include "document this", "write docs", "create readme", "API reference", "user guide", "architecture docs", "ADR", "runbook". For updating existing READMEs, use updating-readme instead.

technical-writing

16
from diegosouzapw/awesome-omni-skill

Write clear technical documentation, tutorials, and guides. Use this skill when creating README files, API docs, setup guides, architecture docs, or technical tutorials.

document-writing-skills

16
from diegosouzapw/awesome-omni-skill

Teaches document writing patterns and templates that agents apply when generating documentation, reports, contracts, guides, and technical writing. Use when creating API docs, user guides, reports, changelogs, ADRs, or technical documentation.