CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/opencode-toolkit

Complete toolkit for configuring and extending OpenCode: agent creation, custom slash commands, configuration management, plugin development, and SDK usage.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

hook-patterns.mdbuild-plugins/references/

Hook Patterns Reference

All hook implementation patterns with examples

patterns

1. Event Hook (Reactive)

Listen to all events, discriminate by type:

return {
  event: async ({ event }) => {
    switch (event.type) {
      case "session.idle":
        console.log("Session completed:", event.properties.sessionID)
        break
      case "file.edited":
        console.log("File changed:", event.properties.file)
        break
    }
  },
}

2. Custom Tools

Register tools the LLM can call:

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

export const MyPlugin: Plugin = async (ctx) => {
  return {
    tool: {
      lint: tool({
        description: "Run ESLint on a file",
        args: {
          file: tool.schema.string().describe("File path to lint"),
          fix: tool.schema.boolean().optional().describe("Auto-fix issues"),
        },
        async execute(args, context) {
          const result = await ctx.$`eslint ${args.fix ? "--fix" : ""} ${args.file}`.quiet()
          return result.text()
        },
      }),
    },
  }
}

3. Tool Execution Hooks

Intercept before/after tool execution:

return {
  // Modify args or throw to block
  "tool.execute.before": async (input, output) => {
    if (input.tool === "read" && output.args.filePath?.includes(".env")) {
      throw new Error("Reading .env files is blocked")
    }
  },

  // Modify output/title/metadata
  "tool.execute.after": async (input, output) => {
    console.log(`Tool ${input.tool} completed`)
    // Modify: output.title, output.output, output.metadata
  },
}

4. Permission Hook

Override permission decisions:

return {
  "permission.ask": async (input, output) => {
    // input: { id, type, pattern, sessionID, messageID, title, metadata }
    // output.status: "ask" | "deny" | "allow"

    if (input.type === "bash" && input.metadata.command?.includes("rm -rf")) {
      output.status = "deny"
    }
  },
}

5. Chat Hooks

Modify messages or LLM parameters:

return {
  // Intercept user messages
  "chat.message": async (input, output) => {
    // input: { sessionID, agent?, model?, messageID? }
    // output: { message: UserMessage, parts: Part[] }
    console.log("User message:", output.message)
  },

  // Modify LLM parameters per request
  "chat.params": async (input, output) => {
    // input: { sessionID, agent, model, provider, message }
    // output: { temperature, topP, topK, options }
    if (input.agent === "creative") {
      output.temperature = 0.9
    }
  },
}

6. Auth Hook

Add custom provider authentication:

return {
  auth: {
    provider: "my-provider",
    methods: [
      {
        type: "api",
        label: "API Key",
        prompts: [
          {
            type: "text",
            key: "apiKey",
            message: "Enter your API key",
            validate: (v) => (v.length < 10 ? "Key too short" : undefined),
          },
        ],
        async authorize(inputs) {
          return { type: "success", key: inputs!.apiKey }
        },
      },
    ],
  },
}

7. Compaction Hook

Customize session compaction:

return {
  "experimental.session.compacting": async (input, output) => {
    // Add context to default prompt
    output.context.push("Remember: user prefers TypeScript")

    // OR replace entire prompt
    output.prompt = "Summarize this session focusing on code changes..."
  },
}

8. Config Hook

Modify configuration on load:

return {
  config: async (config) => {
    // Mutate config object
    config.theme = "dark"
  },
}

quick_reference

Hook Signature Quick Reference

HookSignatureMutate
event({ event }) => voidRead-only
config(config) => voidMutate config
toolObject of tool() definitionsN/A
authAuthHook objectN/A
chat.message(input, output) => voidMutate output
chat.params(input, output) => voidMutate output
permission.ask(input, output) => voidSet output.status
tool.execute.before(input, output) => voidMutate output.args
tool.execute.after(input, output) => voidMutate output
experimental.*(input, output) => voidMutate output

Implementation Procedure

Step 1: Verify SDK Reference (REQUIRED)

Before creating any plugin, regenerate the API reference to ensure accuracy:

bun run .opencode/skill/opencode-build-plugins/scripts/extract-plugin-api.ts

This generates references/hooks.md, references/events.md, references/tool-helper.md.

Step 2: Validate Feasibility

Feasible as plugins:

  • Intercepting/blocking tool calls
  • Reacting to events (file edits, session completion, etc.)
  • Adding custom tools for the LLM
  • Modifying LLM parameters (temperature, etc.)
  • Custom auth flows for providers
  • Customizing session compaction
  • Displaying status messages (toasts, inline)

NOT feasible (inform user):

  • Modifying TUI rendering or layout
  • Adding new built-in tools (requires OC source)
  • Changing core agent behavior/prompts
  • Intercepting assistant responses mid-stream
  • Adding new keybinds or commands
  • Modifying internal file read/write
  • Adding new permission types

If not feasible, inform user clearly. Suggest:

  • OC core changes: contribute to packages/opencode
  • MCP tools: use MCP server configuration
  • Simple automation: use shell scripts

Step 3: Design Plugin

Follow modular design principles from CODING-TS.MD:

  • Modular structure: Split complex plugins into multiple focused files
  • Single purpose: Each function does ONE thing well
  • DRY: Extract common patterns into shared utilities
  • Small files: Keep individual files under 150 lines
  • No monoliths: MUST NOT put all plugin code in a single index.ts

Step 4: Plugin Structure (Non-Monolithic)

For complex plugins, use a modular directory structure:

.opencode/plugins/my-plugin/
├── index.ts          # Entry point, exports Plugin
├── types.ts          # TypeScript types/interfaces
├── utils.ts          # Shared utilities
├── hooks/            # Hook implementations
│   ├── event.ts
│   └── tool-execute.ts
└── tools/            # Custom tool definitions
    └── my-tool.ts

Example modular index.ts:

import type { Plugin } from "@opencode-ai/plugin"
import { eventHooks } from "./hooks/event"
import { toolHooks } from "./hooks/tool-execute"
import { customTools } from "./tools"

export const MyPlugin: Plugin = async ({ project, client }) => {
  return {
    ...eventHooks({ client }),
    ...toolHooks({ client }),
    tool: customTools,
  }
}

Common Plugin Patterns

Security Guard — Block Dangerous Commands

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

const BLOCKED = ["rm -rf", "git push --force", "DROP TABLE"]

export const SecurityPlugin: Plugin = async () => ({
  "tool.execute.before": async (input) => {
    if (input.tool === "bash") {
      const cmd = input.args?.command ?? ""
      if (BLOCKED.some((pattern) => cmd.includes(pattern))) {
        throw new Error(`Blocked dangerous command: ${cmd}`)
      }
    }
  }
})

Audit Logger — Log All Tool Calls

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

export const AuditPlugin: Plugin = async ({ project }) => ({
  "tool.execute.after": async (input, output) => {
    const entry = JSON.stringify({
      ts: new Date().toISOString(),
      tool: input.tool,
      args: input.args,
      success: !output.error
    })
    appendFileSync(".opencode/audit.log", entry + "\n")
  }
})

Session Notifier — Toast on Session End

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

export const NotifyPlugin: Plugin = async ({ client }) => ({
  event: async ({ event }) => {
    if (event.type === "session.completed") {
      await client.tui.showToast({
        message: "Session complete",
        variant: "success"
      })
    }
  }
})

Custom Tool — Expose a New Callable Function

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

const lookupUser = tool({
  description: "Look up a user by email in the internal directory",
  args: {
    email: tool.schema.string().describe("User email address")
  },
  async execute({ email }) {
    const result = await fetch(`https://api.example.com/users?email=${email}`)
    return JSON.stringify(await result.json())
  }
})

export const DirectoryPlugin: Plugin = async () => ({
  tool: { "lookup-user": lookupUser }
})

build-plugins

CHANGELOG.md

SKILL.md

tile.json

CHANGELOG.md

tile.json