Skip to content

Plugins Advanced

💡 One-line summary: Master all hook types and implement advanced plugin features.

📝 Course Notes

Key knowledge points from this lesson:

5.12b Plugins Advanced Notes


What You'll Be Able to Do

  • Understand the difference between event hooks and functional hooks
  • Use all available hook types
  • Create custom tools
  • Implement authentication plugins

Hook Categories

OpenCode plugins have two types of hooks:

TypeCharacteristicsUse Cases
Event HooksPassive listening, no data modificationLogging, notifications, statistics
Functional HooksActive interception, can modify dataPermission control, parameter modification, data transformation

Event Hooks

Subscribe to all events using event:

ts
export const MyPlugin: Plugin = async () => {
  return {
    event: async ({ event }) => {
      console.log(`Event: ${event.type}`, event.properties)
    },
  }
}

Functional Hooks

Intercept specific operations using concrete hook names:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      // Can modify output to affect subsequent execution
      console.log(`Tool: ${input.tool}`)
    },
  }
}

Event Types

All events are subscribed via the event hook and distinguished by event.type:

Command Events

EventTrigger Timing
command.executedAfter slash command execution

File Events

EventTrigger Timing
file.editedAfter file is edited
file.watcher.updatedFile watcher detects changes

Installation Events

EventTrigger Timing
installation.updatedAfter OpenCode installation/update

LSP Events

EventTrigger Timing
lsp.client.diagnosticsLSP diagnostics update
lsp.updatedLSP service status change

Message Events

EventTrigger Timing
message.part.removedMessage part is deleted
message.part.updatedMessage part is updated
message.removedMessage is deleted
message.updatedMessage is updated

Permission Events

EventTrigger Timing
permission.repliedUser responds to permission request
permission.updatedPermission status change

Server Events

EventTrigger Timing
server.connectedServer connection successful

Session Events

EventTrigger Timing
session.createdNew session created
session.compactedSession compaction completed
session.deletedSession is deleted
session.diffSession diff generated
session.errorSession error occurs
session.idleSession enters idle state (AI response complete)
session.statusSession status change
session.updatedSession info update

Todo Events

EventTrigger Timing
todo.updatedTodo list update

TUI Events

EventTrigger Timing
tui.prompt.appendContent appended to prompt
tui.command.executeTUI command execution
tui.toast.showShow toast notification

Functional Hooks Details

config

Triggered after config is loaded, can modify configuration:

ts
export const MyPlugin: Plugin = async () => {
  return {
    config: async (config) => {
      // config: Config object (see config.ts for full type definition)
      // Can directly modify properties, e.g.:
      config.model = "anthropic/claude-opus-4-5-thinking"
    },
  }
}

Parameter Type: config: Config (read/write)

chat.message

Triggered when new message is received, can modify message content:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "chat.message": async (input, output) => {
      // input: { sessionID, agent, model, messageID, variant }
      // output: { message, parts }
      console.log(`New message in session: ${input.sessionID}`)
    },
  }
}

input Type:

FieldTypeDescription
sessionIDstringSession ID
agentstringAgent name
model{ providerID, modelID }Model info
messageIDstringMessage ID
variantstringMessage variant

output Type:

FieldTypeDescription
messageMessageMessage object (modifiable)
partsPart[]Message content parts (modifiable)

chat.params

Triggered before LLM call, can modify model parameters:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "chat.params": async (input, output) => {
      // input: { sessionID, agent, model, provider, message }
      // output: { temperature, topP, topK, options }
      
      // Force low temperature
      output.temperature = 0.3
      
      // Add custom options (passed as HTTP headers)
      output.options.customHeader = "my-value"
    },
  }
}

input Type:

FieldTypeDescription
sessionIDstringSession ID
agentstringAgent name
model{ providerID, modelID }Model info
providerProviderProvider object
messageMessageCurrent message

output Type (modifiable):

FieldTypeDescription
temperaturenumber?Temperature parameter
topPnumber?Top-P parameter
topKnumber?Top-K parameter
optionsRecord<string, unknown>Custom options (passed as HTTP headers)

permission.ask

Triggered on permission request, can modify permission decision:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "permission.ask": async (input, output) => {
      // input: Permission object
      // output: { status: "ask" | "deny" | "allow" }
      
      // Auto-allow specific tools
      if (input.tool === "read" && input.path?.startsWith("/safe/")) {
        output.status = "allow"
      }
    },
  }
}

tool.execute.before

Triggered before tool execution, can modify parameters or throw error to block execution:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      // input: { tool, sessionID, callID }
      // output: { args }
      
      if (input.tool === "bash" && output.args.command.includes("rm -rf")) {
        throw new Error("Dangerous command blocked!")
      }
    },
  }
}

input Type:

FieldTypeDescription
toolstringTool name (e.g., read, bash, write)
sessionIDstringSession ID
callIDstringTool call ID

output Type (modifiable):

FieldTypeDescription
argsRecord<string, unknown>Tool arguments (modifiable or interceptable)

Throwing Error: Throwing Error will block tool execution, error message is returned to LLM.

tool.execute.after

Triggered after tool execution, can modify output:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.after": async (input, output) => {
      // input: { tool, sessionID, callID }
      // output: { title, output, metadata }
      
      // Add execution timestamp
      output.metadata.executedAt = new Date().toISOString()
    },
  }
}

input Type:

FieldTypeDescription
toolstringTool name
sessionIDstringSession ID
callIDstringTool call ID

output Type (modifiable):

FieldTypeDescription
titlestringTool execution title (displayed in UI)
outputstringTool output content (returned to LLM)
metadataRecord<string, unknown>Metadata (freely addable)

Experimental Hooks

⚠️ Warning: The following hooks are prefixed with experimental. and API may change in future versions.

experimental.session.compacting

Triggered before session compaction, can customize compaction context:

ts
export const CompactionPlugin: Plugin = async () => {
  return {
    "experimental.session.compacting": async (input, output) => {
      // input: { sessionID }
      // output: { context: string[], prompt?: string }
      
      // Method 1: Append extra context
      output.context.push(`
## Custom Context

Preserve the following state:
- Current task status
- Important decisions
- Files being processed
`)
    },
  }
}

Completely replace compaction prompt:

ts
export const CustomCompactionPlugin: Plugin = async () => {
  return {
    "experimental.session.compacting": async (input, output) => {
      // Setting prompt completely replaces default compaction prompt
      // output.context array will be ignored
      output.prompt = `
You are generating a continuation prompt for a multi-agent session.

Please summarize:
1. Current task and its status
2. Files being modified and who is responsible
3. Dependencies between agents
4. Next steps to complete the work

Format as a structured prompt that a new agent can use to resume work.
`
    },
  }
}

experimental.chat.messages.transform

Triggered before messages are sent to LLM, can transform message list:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.chat.messages.transform": async (input, output) => {
      // output.messages: Array<{ info: Message, parts: Part[] }>
      
      // Filter certain messages
      output.messages = output.messages.filter(m => 
        !m.parts.some(p => p.type === "text" && p.text.includes("SECRET"))
      )
    },
  }
}

experimental.chat.system.transform

Triggered before system prompt is sent to LLM:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.chat.system.transform": async (input, output) => {
      // output.system: string[]
      
      // Append custom system instructions
      output.system.push("Always respond in formal English.")
    },
  }
}

experimental.text.complete

Triggered after text completion:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.text.complete": async (input, output) => {
      // input: { sessionID, messageID, partID }
      // output: { text }
      
      // Can modify the final output text
      output.text = output.text.replace(/\bAI\b/g, "Assistant")
    },
  }
}

Custom Tools

Plugins can add custom tools for AI to call:

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

export const CustomToolsPlugin: Plugin = async () => {
  return {
    tool: {
      mytool: tool({
        description: "This is a custom tool",
        args: {
          foo: tool.schema.string().describe("Input parameter"),
          count: tool.schema.number().optional().describe("Optional number parameter"),
        },
        async execute(args, ctx) {
          // args: { foo: string, count?: number }
          // ctx: ToolContext
          return `Hello ${args.foo}!`
        },
      }),
    },
  }
}

tool Function Parameters

ParameterTypeDescription
descriptionstringTool description, AI decides when to use based on this
argsRecord<string, ZodType>Define parameters using Zod schema
execute(args, ctx) => Promise<string>Tool execution function

ToolContext

The second parameter of execute function provides execution context:

PropertyTypeDescription
sessionIDstringCurrent session ID
messageIDstringCurrent message ID
agentstringAgent name calling the tool
abortAbortSignalAbort signal for canceling long operations

Using abort Signal

ts
tool({
  description: "Long running task",
  args: {},
  async execute(args, ctx) {
    for (let i = 0; i < 100; i++) {
      if (ctx.abort.aborted) {
        return "Task cancelled"
      }
      await doWork(i)
    }
    return "Task completed"
  },
})

Zod Schema Quick Reference

tool.schema is Zod, common types:

ts
tool.schema.string()           // String
tool.schema.number()           // Number
tool.schema.boolean()          // Boolean
tool.schema.array(...)         // Array
tool.schema.object({...})      // Object
tool.schema.enum(["a", "b"])   // Enum
tool.schema.optional()         // Optional (chained)
tool.schema.describe("...")    // Description (chained)

Authentication Hooks

Plugins can implement custom authentication for providers:

ts
export const MyAuthPlugin: Plugin = async () => {
  return {
    auth: {
      provider: "my-provider",
      
      // Optional: Load config from existing auth
      loader: async (auth, provider) => {
        const token = await auth()
        return { apiKey: token.key }
      },
      
      methods: [
        {
          type: "api",
          label: "API Key",
          prompts: [
            {
              type: "text",
              key: "apiKey",
              message: "Enter your API key",
              validate: (value) => value.length < 10 ? "Key too short" : undefined,
            },
          ],
          authorize: async (inputs) => {
            // Validate and return result
            return {
              type: "success",
              key: inputs.apiKey,
            }
          },
        },
        {
          type: "oauth",
          label: "OAuth Login",
          authorize: async () => {
            return {
              url: "https://example.com/oauth/authorize",
              instructions: "Complete login in browser",
              method: "auto",
              callback: async () => {
                // Wait for OAuth callback
                return {
                  type: "success",
                  access: "access_token",
                  refresh: "refresh_token",
                  expires: Date.now() + 3600000,
                }
              },
            }
          },
        },
      ],
    },
  }
}

Authentication Method Types

TypeDescription
apiAPI Key method, user directly enters key
oauthOAuth method, redirects to browser for authorization

prompts Configuration

TypeDescription
textText input field
selectDropdown select

Each prompt can configure:

  • key: Key name for input value
  • message: Prompt message
  • validate: Validation function
  • condition: Condition function to determine whether to show this prompt

Complete Example: Time Tracking Plugin

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

export const TimeTrackingPlugin: Plugin = async ({ client }) => {
  const sessionTimes = new Map<string, number>()

  return {
    event: async ({ event }) => {
      if (event.type === "session.created") {
        sessionTimes.set(event.properties.id, Date.now())
        await client.app.log({
          service: "time-tracking",
          level: "info",
          message: `Session started: ${event.properties.id}`,
        })
      }
      
      if (event.type === "session.idle") {
        const startTime = sessionTimes.get(event.properties.sessionID)
        if (startTime) {
          const duration = Date.now() - startTime
          await client.app.log({
            service: "time-tracking",
            level: "info",
            message: `Session duration: ${Math.round(duration / 1000)}s`,
            extra: { sessionID: event.properties.sessionID, duration },
          })
        }
      }
    },
    
    "chat.params": async (input, output) => {
      // Add tracking headers to all requests
      output.options["X-Session-ID"] = input.sessionID
    },
  }
}

Common Pitfalls

SymptomCauseSolution
Hook doesn't triggerFunction name typoUse TypeScript for type checking
output modification ineffectiveReturned new object instead of modifying originalDirectly modify output.xxx = ...
Experimental hook failsAPI changed after version updateCheck changelog, adjust code
Auth plugin ineffectiveprovider name mismatchEnsure it matches provider ID in config
abort signal not respondingNot checking ctx.abort.abortedPeriodically check in long loops

Lesson Summary

You learned:

  1. The difference between event hooks and functional hooks
  2. All available hook types and their use cases
  3. Creating custom tools (with abort signal handling)
  4. Implementing authentication plugins