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.

98

Quality

98%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

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

SKILL.md

tile.json

tile.json