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