DEV Community

Cover image for Does OpenCode Support Hooks? A Complete Guide to Extensibility
Einar César
Einar César

Posted on

Does OpenCode Support Hooks? A Complete Guide to Extensibility

If you're working with OpenCode - the AI coding agent built for the terminal - you might be wondering: "Can I hook into OpenCode's behavior? Can I automate tasks or extend its functionality?"

The short answer: Yes, absolutely! OpenCode has a robust, native hook system that many developers don't know about yet.

What is OpenCode?

Before we dive into hooks, let's quickly cover what OpenCode is. OpenCode is an AI coding assistant that runs in your terminal, similar to Claude Code or GitHub Copilot, but with a key difference: it's open-source and highly extensible. It helps you write code, debug issues, refactor projects, and automate development tasks using AI.

Why Would You Need Hooks?

Imagine you want to:

  • 🔒 Prevent OpenCode from reading sensitive files like .env
  • Automatically format code after OpenCode edits it
  • 📢 Send notifications to Slack when a coding session completes
  • 🔍 Log all AI interactions for compliance or analysis
  • 🚀 Trigger deployments after successful code changes
  • 🧪 Run tests automatically when files are modified

All of this is possible with OpenCode's hook system. Let me show you how.

Six Ways to Implement Hooks in OpenCode

OpenCode provides six different mechanisms for hooks and extensibility. You can choose the one that fits your use case:

1. Plugin System (Most Powerful)

The plugin system is your main tool for implementing hooks. Plugins are JavaScript or TypeScript files that can intercept events and tool executions.

When to use: Complex logic, event handling, custom validations

Example - Protect sensitive files:

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

export const EnvProtection: Plugin = async ({ client }) => {
  return {
    tool: {
      execute: {
        before: async (input, output) => {
          // This runs BEFORE any tool executes
          if (input.tool === "read" && 
              output.args.filePath.includes(".env")) {
            throw new Error("🚫 Cannot read .env files")
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Save this as .opencode/plugin/env-protection.ts and it automatically prevents OpenCode from reading environment files!

Example - Auto-format code after edits:

export const AutoFormat: Plugin = async ({ $ }) => {
  return {
    tool: {
      execute: {
        after: async (input, output) => {
          // This runs AFTER the tool completes
          if (input.tool === "edit") {
            await $`prettier --write ${output.args.filePath}`
            console.log("✨ Code formatted!")
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. SDK with Event Streaming

The SDK gives you programmatic control over OpenCode from external applications. It uses Server-Sent Events (SSE) to stream what's happening in real-time.

When to use: External integrations, monitoring, CI/CD automation

Example - Monitor sessions and send notifications:

import { createOpencodeClient } from "@opencode-ai/sdk"

const client = createOpencodeClient({
  baseUrl: "http://localhost:4096"
})

// Subscribe to all events
const eventStream = await client.event.subscribe()

for await (const event of eventStream) {
  console.log("📡 Event:", event.type)

  if (event.type === "session.idle") {
    // Session completed - send notification
    await sendSlackMessage("OpenCode session completed!")
  }
}
Enter fullscreen mode Exit fullscreen mode

Start the OpenCode server: opencode serve --port 4096

Install SDK: npm install @opencode-ai/sdk

3. MCP Servers (Model Context Protocol)

MCP servers let you add external tools and capabilities that OpenCode can use during coding sessions.

When to use: Adding new tools, integrating third-party services

Example configuration in opencode.json:

{
  "mcp": {
    "database-query": {
      "type": "local",
      "command": ["node", "./mcp-servers/database.js"],
      "enabled": true
    },
    "company-docs": {
      "type": "remote",
      "url": "https://docs.mycompany.com/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Real-world MCP servers you can use:

  • Appwrite - Backend services
  • Context7 - Documentation search
  • Bright Data - Web scraping
  • Playwright - Browser automation

4. GitHub Integration

The GitHub integration works like webhooks for your repositories. OpenCode automatically responds to comments in issues and PRs.

When to use: PR automation, issue triage, code review

Setup:

opencode github install
Enter fullscreen mode Exit fullscreen mode

Usage in GitHub:

  • Comment /opencode explain this issue on any issue
  • Comment /opencode fix this bug to create a branch and PR
  • Comment /opencode review these changes on a PR

Manual workflow configuration:

name: opencode
on:
  issue_comment:
    types: [created]

jobs:
  opencode:
    if: contains(github.event.comment.body, '/opencode')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: sst/opencode/github@latest
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        with:
          model: anthropic/claude-sonnet-4-20250514
Enter fullscreen mode Exit fullscreen mode

5. Custom Commands

Custom commands are reusable prompts saved as Markdown files. They can execute shell commands and accept arguments.

When to use: Common tasks, templates, simple automation

Example .opencode/command/test.md:

---
description: "Run tests with coverage"
---
Run the full test suite with coverage:
`!npm test -- --coverage`

Analyze the results and suggest improvements for:
- Test coverage gaps
- Slow tests
- Flaky tests
Enter fullscreen mode Exit fullscreen mode

Usage: Type /test in OpenCode to run this command.

With arguments:

---
description: Test a specific component
---
Run tests for the $COMPONENT component:
`!npm test -- $COMPONENT.test.ts`

Review the results and suggest fixes.
Enter fullscreen mode Exit fullscreen mode

Usage: /test Button automatically substitutes $COMPONENT with Button

6. Non-Interactive Mode

The non-interactive mode lets you script OpenCode for automation without the TUI interface.

When to use: CI/CD pipelines, pre-commit hooks, batch processing

Examples:

# Run a command and get JSON output
opencode run "analyze code quality" -f json -q

# Continue a previous session
opencode run "implement the fixes" -c session-id

# Use in a pre-commit hook
opencode run "review my changes and ensure no secrets are committed" -q
Enter fullscreen mode Exit fullscreen mode

Complete Comparison Table

Mechanism Complexity Flexibility Best For
Plugin System High Very High Custom logic, event hooks, validations
SDK/API Medium Very High Full programmatic control, integrations
MCP Servers Medium High External tools, third-party protocols
GitHub Integration Low Medium PR/issue workflows, repository automation
Custom Commands Low Low Reusable prompts, simple automation
Non-Interactive Low Medium CI/CD, scripts, batch processing

Practical Use Cases with Code

Security: Prevent Reading Sensitive Files

export const SecurityPlugin: Plugin = async ({ client }) => {
  const sensitivePatterns = ['.env', 'secret', 'credentials', 'private-key']

  return {
    tool: {
      execute: {
        before: async (input, output) => {
          if (input.tool === "read") {
            const filePath = output.args.filePath.toLowerCase()

            if (sensitivePatterns.some(pattern => filePath.includes(pattern))) {
              throw new Error(`🚫 Blocked: Cannot read sensitive file ${output.args.filePath}`)
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Quality: Auto-format and Test After Edits

export const QualityPlugin: Plugin = async ({ $ }) => {
  return {
    tool: {
      execute: {
        after: async (input, output) => {
          if (input.tool === "edit") {
            const file = output.args.filePath

            // Format the file
            await $`prettier --write ${file}`
            console.log("✨ Formatted:", file)

            // Run tests
            const result = await $`npm test ${file}`.quiet()
            if (result.exitCode !== 0) {
              console.warn("⚠️  Tests failed for:", file)
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notifications: Slack Integration

export const SlackNotifier: Plugin = async () => {
  return {
    event: async ({ event }) => {
      if (event.type === "session.idle") {
        await fetch(process.env.SLACK_WEBHOOK_URL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            text: `✅ OpenCode session completed!`,
            blocks: [{
              type: "section",
              text: {
                type: "mrkdwn",
                text: `*Session ID:* ${event.properties.sessionId}\n*Files modified:* ${event.properties.filesModified}`
              }
            }]
          })
        })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Automated Code Review with SDK

import { createOpencodeClient } from "@opencode-ai/sdk"

async function autoReview() {
  const client = createOpencodeClient({
    baseUrl: "http://localhost:4096"
  })

  // Create a new session
  const session = await client.session.create({
    title: "Automated Code Review"
  })

  // Get modified files
  const files = await client.file.status()
  const modifiedFiles = files.filter(f => f.status === "modified")

  // Review each file
  for (const file of modifiedFiles) {
    const content = await client.file.read({ path: file.path })

    await client.session.chat({
      id: session.id,
      providerID: "anthropic",
      modelID: "claude-sonnet-4-20250514",
      parts: [{
        type: "text",
        text: `Review this file for:
- Code quality issues
- Security vulnerabilities
- Performance problems
- Best practice violations

File: ${file.path}
\`\`\`
${content}
\`\`\``
      }]
    })
  }

  // Share the review
  const shared = await client.session.share({ id: session.id })
  console.log("📊 Review URL:", shared.shareUrl)
}
Enter fullscreen mode Exit fullscreen mode

Getting Started: Step-by-Step

1. Install OpenCode

npm install -g @opencode-ai/cli
Enter fullscreen mode Exit fullscreen mode

2. Create Your First Plugin

# Create plugin directory
mkdir -p .opencode/plugin

# Install plugin types
npm install @opencode-ai/plugin
Enter fullscreen mode Exit fullscreen mode

Create .opencode/plugin/my-first-plugin.ts:

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

export const MyFirstPlugin: Plugin = async ({ app, client, $ }) => {
  console.log("🎉 Plugin loaded!")

  return {
    event: async ({ event }) => {
      console.log("📡 Event:", event.type)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Enable Your Plugin

Create/edit opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": {
    "my-first-plugin": {
      "enabled": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Test It

opencode
# You should see: 🎉 Plugin loaded!
Enter fullscreen mode Exit fullscreen mode

Configuration Priority

OpenCode looks for configuration in this order:

  1. OPENCODE_CONFIG environment variable
  2. ./opencode.json (project directory)
  3. ~/.config/opencode/opencode.json (global)

Community Demand and Real-World Usage

The OpenCode community actively requested hooks before the current system was fully documented:

  • Issue #1473 "Hooks support?" - Developer wanted to automate commits and PRs after tasks
  • Issue #753 "Extensible Plugin System" - Proposed complete architecture with lifecycle, conversation, and tool call hooks
  • Issue #2185 "Hooks for commands" - Requested command-level hooks for pre/post LLM processing

Community integrations:

  • opencode.nvim - Forwards OpenCode events as Neovim autocmds
  • opencode-mcp-tool - MCP server to control OpenCode from other systems
  • Context7 MCP - Documentation search integration
  • Bright Data Web MCP - Advanced web scraping

Developers on X/Twitter share custom hook implementations regularly. Articles on Medium and DEV Community rank OpenCode above Claude Code and Aider specifically for plugin and hook flexibility.

What OpenCode Doesn't Have

For completeness, OpenCode does NOT have:

  • ❌ Traditional HTTP webhooks (POST endpoints)
  • ❌ Direct git hooks (pre-commit, post-commit)
  • ❌ Webhook receiver endpoints for external services

But the event-driven internal hooks cover the same use cases in a more integrated way.

Documentation Resources

Conclusion

OpenCode has a mature, production-ready hook system that many developers don't know about yet. Whether you need simple automation with custom commands or sophisticated event-driven workflows with plugins and the SDK, OpenCode has you covered.

Start simple:

  1. Use custom commands for repetitive tasks
  2. Add plugins when you need custom logic
  3. Use the SDK for external integrations
  4. Add MCP servers for new capabilities
  5. Enable GitHub integration for repository automation

The architecture allows you to combine multiple mechanisms as needed, offering exceptional flexibility without requiring any workarounds.

Have you built something cool with OpenCode hooks? Share your experience in the comments! 👇


Found this helpful? Give it a ❤️ and follow!

Top comments (0)