Skip to main content

Overview

OpenCode’s plugin system allows you to extend functionality by:
  • Creating custom tools for the AI agent
  • Hooking into the chat lifecycle
  • Modifying prompts and parameters
  • Handling authentication for custom providers
  • Responding to events
Plugins are TypeScript/JavaScript modules that export a plugin function returning hooks.

Installation

Install the plugin SDK:
npm install @opencode-ai/plugin

Basic Plugin

Create a plugin file (e.g., my-plugin.ts):
import { Plugin } from '@opencode-ai/plugin'

export const MyPlugin: Plugin = async (ctx) => {
  // ctx provides access to client, project, directory, etc.
  
  return {
    // Return hooks and tools
    tool: {
      // Custom tools
    },
    event: async (input) => {
      // Handle events
    },
  }
}

Plugin Input

The plugin function receives a context object:
type PluginInput = {
  client: OpencodeClient    // SDK client instance
  project: Project          // Current project
  directory: string         // Project directory
  worktree: string          // Project worktree root
  serverUrl: URL            // Server URL
  $: BunShell              // Shell for running commands
}

Using the Context

export const MyPlugin: Plugin = async (ctx) => {
  // Access the SDK client
  const config = await ctx.client.config.get()
  console.log('Current model:', config.data.model)
  
  // Get project info
  console.log('Project:', ctx.project.id)
  console.log('Directory:', ctx.directory)
  
  // Run shell commands
  const result = await ctx.$`git status`
  console.log(result.stdout.toString())
  
  return {
    // ... hooks
  }
}

Registering Plugins

Add your plugin to opencode.json:
{
  "plugin": [
    "./my-plugin.ts",
    "@company/opencode-plugin"
  ]
}
Or programmatically:
import { createOpencode } from '@opencode-ai/sdk'

const { client, server } = await createOpencode({
  config: {
    plugin: ['./my-plugin.ts'],
  },
})

Creating Tools

Tools are functions that the AI agent can call. Use the tool() helper to define them:
import { Plugin, tool } from '@opencode-ai/plugin'

export const MyPlugin: Plugin = async (ctx) => {
  return {
    tool: {
      search_database: tool({
        description: 'Search the database for records',
        args: {
          query: tool.schema.string().describe('Search query'),
          limit: tool.schema.number().optional().describe('Max results'),
        },
        async execute(args, context) {
          // args.query and args.limit are typed
          const results = await searchDB(args.query, args.limit)
          return JSON.stringify(results)
        },
      }),
    },
  }
}

Tool Definition

function tool<Args extends z.ZodRawShape>(input: {
  description: string
  args: Args
  execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string>
})

Tool Context

The execute function receives a context object:
type ToolContext = {
  sessionID: string      // Current session
  messageID: string      // Current message
  agent: string          // Current agent name
  directory: string      // Project directory
  worktree: string       // Project worktree root
  abort: AbortSignal     // Cancellation signal
  
  // Update tool metadata
  metadata(input: {
    title?: string
    metadata?: Record<string, any>
  }): void
  
  // Request permission
  ask(input: {
    permission: string
    patterns: string[]
    always: string[]
    metadata: Record<string, any>
  }): Promise<void>
}

Tool Schema

Use Zod for argument validation:
import { tool } from '@opencode-ai/plugin'

tool({
  description: 'Example tool with various argument types',
  args: {
    // String
    name: tool.schema.string().describe('User name'),
    
    // Number
    age: tool.schema.number().min(0).max(150).describe('User age'),
    
    // Boolean
    active: tool.schema.boolean().describe('Is active'),
    
    // Optional
    email: tool.schema.string().email().optional().describe('Email address'),
    
    // Enum
    role: tool.schema.enum(['admin', 'user', 'guest']).describe('User role'),
    
    // Array
    tags: tool.schema.array(tool.schema.string()).describe('Tags'),
    
    // Object
    metadata: tool.schema.object({
      key: tool.schema.string(),
      value: tool.schema.string(),
    }).describe('Metadata'),
  },
  async execute(args, context) {
    // args is fully typed
    return 'Result'
  },
})
See Plugin Tools for detailed tool documentation.

Hooks

Plugins can implement various hooks to customize behavior:

Event Hook

Listen to all server events:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    event: async (input) => {
      const { event } = input
      
      if (event.type === 'session.created') {
        console.log('New session:', event.properties.info.id)
      }
      
      if (event.type === 'message.updated') {
        console.log('Message updated:', event.properties.info.id)
      }
    },
  }
}

Config Hook

Modify configuration:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    config: async (config) => {
      console.log('Config loaded:', config.model)
      // Can modify config here
    },
  }
}

Chat Hooks

chat.message

Called when a new message is received:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'chat.message': async (input, output) => {
      const { sessionID, agent, model } = input
      const { message, parts } = output
      
      console.log(`Message in session ${sessionID}:`)
      console.log(`Agent: ${agent}`)
      console.log(`Model: ${model?.providerID}/${model?.modelID}`)
      console.log(`Parts: ${parts.length}`)
    },
  }
}

chat.params

Modify LLM parameters:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'chat.params': async (input, output) => {
      const { agent, model } = input
      
      // Adjust temperature based on agent
      if (agent === 'build') {
        output.temperature = 0.7
      } else if (agent === 'plan') {
        output.temperature = 0.3
      }
      
      // Add custom options
      output.options.customParam = 'value'
    },
  }
}

chat.headers

Add custom headers to LLM requests:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'chat.headers': async (input, output) => {
      output.headers['X-Custom-Header'] = 'value'
      output.headers['X-Session-ID'] = input.sessionID
    },
  }
}

Permission Hook

Control permission requests:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'permission.ask': async (permission, output) => {
      // Auto-approve certain patterns
      if (permission.type === 'bash' && permission.pattern?.includes('npm')) {
        output.status = 'allow'
      }
      
      // Deny dangerous operations
      if (permission.pattern?.includes('rm -rf')) {
        output.status = 'deny'
      }
    },
  }
}

Command Hook

Run code before command execution:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'command.execute.before': async (input, output) => {
      const { command, sessionID, arguments: args } = input
      
      console.log(`Executing command: ${command} ${args}`)
      
      // Add context parts
      output.parts.push({
        type: 'text',
        text: `Additional context for ${command}`,
      })
    },
  }
}

Tool Hooks

tool.execute.before

Called before tool execution:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'tool.execute.before': async (input, output) => {
      const { tool, sessionID, callID } = input
      
      console.log(`Tool ${tool} called`)
      
      // Modify arguments
      output.args.modified = true
    },
  }
}

tool.execute.after

Called after tool execution:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'tool.execute.after': async (input, output) => {
      const { tool, args } = input
      
      // Modify output
      output.output += '\n\nProcessed by plugin'
      output.metadata.processed = true
    },
  }
}

tool.definition

Modify tool definitions sent to the LLM:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'tool.definition': async (input, output) => {
      if (input.toolID === 'bash') {
        // Make bash tool description more specific
        output.description += ' Use this for running shell commands.'
      }
    },
  }
}

Shell Environment Hook

Customize shell environment:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'shell.env': async (input, output) => {
      // Add custom environment variables
      output.env.CUSTOM_VAR = 'value'
      output.env.PATH = `/custom/path:${output.env.PATH}`
    },
  }
}

Auth Hook

Handle authentication for custom providers:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    auth: {
      provider: 'my-provider',
      methods: [
        {
          type: 'api',
          label: 'API Key',
          prompts: [
            {
              type: 'text',
              key: 'apiKey',
              message: 'Enter your API key',
              validate: (value) => {
                if (!value.startsWith('sk-')) {
                  return 'Invalid API key format'
                }
              },
            },
          ],
          async authorize(inputs) {
            // Validate the API key
            const isValid = await validateKey(inputs.apiKey)
            if (isValid) {
              return { type: 'success', key: inputs.apiKey }
            }
            return { type: 'failed' }
          },
        },
        {
          type: 'oauth',
          label: 'OAuth',
          async authorize() {
            const authUrl = 'https://auth.example.com'
            return {
              url: authUrl,
              instructions: 'Open this URL to authenticate',
              method: 'auto',
              async callback() {
                // Handle OAuth callback
                const tokens = await waitForCallback()
                return {
                  type: 'success',
                  refresh: tokens.refresh,
                  access: tokens.access,
                  expires: tokens.expires,
                }
              },
            }
          },
        },
      ],
    },
  }
}

Experimental Hooks

These hooks may change in future versions:

experimental.chat.messages.transform

Transform messages before sending to LLM:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'experimental.chat.messages.transform': async (input, output) => {
      // Modify messages array
      output.messages = output.messages.filter(
        (msg) => msg.info.role === 'user'
      )
    },
  }
}

experimental.chat.system.transform

Transform system prompt:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'experimental.chat.system.transform': async (input, output) => {
      // Add to system prompt
      output.system.push('Additional system instructions')
    },
  }
}

experimental.session.compacting

Customize session compaction:
export const MyPlugin: Plugin = async (ctx) => {
  return {
    'experimental.session.compacting': async (input, output) => {
      // Add context for compaction
      output.context.push('Important context to preserve')
      
      // Or replace the entire prompt
      output.prompt = 'Custom compaction prompt'
    },
  }
}

Complete Example

Here’s a complete plugin with multiple features:
import { Plugin, tool } from '@opencode-ai/plugin'

export const DatabasePlugin: Plugin = async (ctx) => {
  // Initialize database connection
  const db = await connectDB(ctx.directory)
  
  return {
    // Custom tools
    tool: {
      query_db: tool({
        description: 'Query the database',
        args: {
          sql: tool.schema.string().describe('SQL query'),
        },
        async execute(args, context) {
          const results = await db.query(args.sql)
          return JSON.stringify(results, null, 2)
        },
      }),
    },
    
    // Listen to events
    event: async (input) => {
      if (input.event.type === 'session.created') {
        // Log new sessions to database
        await db.logSession(input.event.properties.info)
      }
    },
    
    // Customize chat parameters
    'chat.params': async (input, output) => {
      // Use lower temperature for database queries
      if (input.agent === 'database') {
        output.temperature = 0.1
      }
    },
    
    // Add custom context
    'command.execute.before': async (input, output) => {
      if (input.command === 'query') {
        // Add database schema context
        const schema = await db.getSchema()
        output.parts.push({
          type: 'text',
          text: `Database schema:\n${schema}`,
        })
      }
    },
  }
}

Next Steps

Plugin Tools

Detailed tool creation guide

Plugin Examples

Real-world plugin examples