Skip to content

Hook 教程

📝 课程笔记

本课核心知识点整理:

Hook 教程学霸笔记


💡 一句话总结:Hook 是 OpenCode 的"扩展接口",你可以在事件发生时执行逻辑,或在关键流程中拦截并修改数据。


学完你能做什么

不吹牛,只写「立刻能干」的事

  • 知道 OpenCode 支持哪些 Hook(插件 Hook / 配置 Hook)
  • 选择正确的 Hook:事件监听 vs 功能拦截
  • 写出常见 Hook:通知、审计、安全拦截、参数调优、上下文压缩增强

你现在的困境

如果你正在经历这些,这课就是给你的

  • 想在会话完成后自动运行脚本,但不知道该在哪里配置
  • 想禁止 AI 读取某些敏感文件,找不到合适的地方拦截
  • 看到别人提到"Hook",不知道和插件是什么关系
  • 想根据不同 Agent 自动调整 LLM 参数,不知道从哪里入手

什么时候用这一招

不是每天都用,但用到就很爽

  • 当你需要:
    • 在特定事件发生时执行自定义逻辑(通知、日志、审计)
    • 拦截工具调用并修改参数或阻止执行
    • 修改 LLM 调用参数(温度、top_p 等)
    • 自定义权限决策逻辑
    • 增强会话压缩的上下文
  • 而且不想:
    • 修改 OpenCode 源码
    • 每次都手动执行这些操作

🎒 开始前的准备

确保你已经完成以下事项,否则请先停下

  • [ ] 完成了 5.12a 插件基础
  • [ ] 完成了 5.12b 插件进阶(推荐)
  • [ ] 有一个运行中的 OpenCode 项目
  • [ ] 可以访问 ~/.config/opencode/ 或项目目录的 .opencode/ 文件夹

核心思路

先讲「怎么想」,不讲命令

  • Hook 本质是一组"可插拔的回调函数"
  • OpenCode 在特定时机触发 Hook,把控制权交给你
  • 有两种 Hook 途径:
    • 插件 Hook:写代码,返回 hooks 对象(更强大、更灵活)
    • 配置 Hook:在 opencode.json 里配置命令(更简单,但功能有限)
  • 事件 Hook 被动监听,不做修改(日志、通知)
  • 功能 Hook 主动拦截,可以修改数据(参数改写、权限控制)

跟我做

一步一步来,假设你会犯错

第 1 步:创建你的第一个插件 Hook

为什么
先做一个最简单的会话完成通知,验证整个流程能跑通。

bash
# 在项目目录创建插件文件
mkdir -p .opencode/plugin
ts
// .opencode/plugin/notify.ts
import type { Plugin } from "@opencode-ai/plugin"

export const NotifyPlugin: Plugin = async ({ $ }) => {
  return {
    event: async ({ event }) => {
      if (event.type === "session.idle") {
        await $`osascript -e 'display notification "会话已完成" with title "OpenCode"'`
      }
    },
  }
}

你应该看到
OpenCode 启动时会加载这个插件,会话完成后会弹出通知。


第 2 步:实现敏感文件拦截

为什么
tool.execute.before Hook 拦截工具调用,阻止 AI 读取敏感文件。

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

export const GuardPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      if (input.tool !== "read") return

      const filePath = String(output.args.filePath)
      const sensitivePatterns = [".env", ".pem", ".key", "credentials"]

      for (const pattern of sensitivePatterns) {
        if (filePath.includes(pattern)) {
          throw new Error(`安全策略:禁止读取敏感文件:${filePath}`)
        }
      }
    },
  }
}

你应该看到
尝试让 AI 读取 .env 文件时,会抛出错误并阻止执行。


第 3 步:根据 Agent 调整 LLM 参数

为什么
不同场景需要不同的参数配置,用 chat.params Hook 自动调整。

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

export const ParamsPlugin: Plugin = async () => {
  return {
    "chat.params": async (input, output) => {
      // 代码生成需要更确定性的输出
      if (input.agent === "code") {
        output.temperature = 0.2
      }

      // 规划任务需要更多创造性
      if (input.agent === "plan") {
        output.temperature = 0.7
      }

      // 添加自定义追踪头
      output.options["X-Trace-Session"] = input.sessionID
    },
  }
}

你应该看到
不同 Agent 会话的 LLM 参数自动变化。


第 4 步:自动决策权限请求

为什么
减少手动确认次数,对安全的操作自动批准。

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

export const AutoPermitPlugin: Plugin = async () => {
  return {
    "permission.ask": async (input, output) => {
      // 读取操作自动允许
      if (input.tool === "read") {
        output.status = "allow"
        return
      }

      // 危险命令自动拒绝
      if (input.tool === "bash" && String(input.metadata?.command).includes("rm -rf")) {
        output.status = "deny"
        return
      }

      // 其他操作保持询问
      output.status = "ask"
    },
  }
}

你应该看到
读取文件不再弹出权限提示,但删除命令会被阻止。


第 5 步:增强会话压缩上下文

为什么
当对话过长需要压缩时,注入项目特定的关键信息。

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

export const CompactionPlugin: Plugin = async () => {
  return {
    "experimental.session.compacting": async (input, output) => {
      output.context.push(`
## 项目关键信息
- 正在修改的文件:src/**
- 关键约束:禁止读取 .env、密钥文件
- 当前任务:实现 Hook 教程并加入侧边栏
- 重要决策:使用 TypeScript 严格模式
`)
    },
  }
}

你应该看到
当对话被压缩时,压缩后的上下文会包含你的自定义信息。


检查点 ✅

全部通过才能继续;任一项失败,回到第 X 步重来

  • [ ] 插件文件放在 .opencode/plugin/ 目录
  • [ ] OpenCode 启动时加载了插件(查看启动日志)
  • [ ] 会话完成后收到了通知
  • [ ] 尝试读取 .env 时抛出了错误
  • [ ] 不同 Agent 会话的参数有变化
  • [ ] 权限请求的行为符合预期

踩坑提醒

80% 的人会卡在这里

现象原因解决
插件未加载文件扩展名错误确保是 .ts.js 文件
output 修改无效返回了新对象而非修改原对象直接修改 output.xxx = ...
事件没触发event.type 拼写错误用 TypeScript 获得类型提示
实验性 Hook 失效版本更新后 API 变化查看更新日志,调整代码
配置 Hook 不生效可能未实现执行逻辑优先使用插件 Hook
多个插件冲突Hook 重复定义检查是否有重复的 Hook 实现

本课小结

你学会了:

  1. 理解 Hook 的两种类型(插件 Hook / 配置 Hook)
  2. 选择合适的 Hook 类型解决问题
  3. 实现常见的 Hook 场景(通知、拦截、调参、权限、压缩)
  4. 遵循 Hook 编写的最佳实践

下一课预告

下一课我们将学习自定义工具,需要用到本课的 Hook 和插件知识。


快速查阅:常用 Hook

开发时最常用的 10 个 Hook,快速参考

Hook触发时机用途是否可修改数据
event所有事件统一订阅,日志/通知/统计
config配置加载后初始化插件,修改配置
tool.execute.before工具执行前拦截/修改参数,阻止执行
tool.execute.after工具执行后记录结果,修改输出
chat.message新消息接收时记录/修改消息内容
chat.paramsLLM 调用前修改温度/Top-P/Top-K
permission.ask权限请求时自动允许/拒绝
tool注册工具添加自定义工具-
experimental.session.compacting会话压缩前注入项目关键信息
auth认证流程自定义认证方式-

快速查阅:常用事件

开发时最常用的 10 个事件,快速参考

事件说明Hook 用途
session.idle会话完成(空闲)发送通知、清理资源、记录耗时
session.created新会话创建初始化会话级状态
file.edited文件被编辑触发格式化、触发构建
message.updated消息更新记录对话历史、统计
tool.execute.after工具执行后记录日志、审计追踪
tool.execute.before工具执行前参数验证、权限检查
permission.replied用户响应权限记录权限决策
command.executed命令执行后命令审计
session.error会话错误错误上报、通知
server.connected服务器连接连接状态通知

附录:源码参考

点击展开查看源码位置
功能文件路径行号
Hook 类型定义packages/plugin/src/index.ts148-218
插件加载逻辑packages/opencode/src/plugin/index.ts20-82
插件目录扫描packages/opencode/src/config/config.ts322-335
插件去重逻辑packages/opencode/src/config/config.ts369-387
配置 Hook Schemapackages/opencode/src/config/config.ts1009-1030

关键代码片段

typescript
// Hook 类型定义
export interface Hooks {
  event?: (input: { event: Event }) => Promise<void>
  config?: (input: Config) => Promise<void>
  tool?: { [key: string]: ToolDefinition }
  auth?: AuthHook
  "chat.message"?: (input: {...}, output: {...}) => Promise<void>
  "chat.params"?: (input: {...}, output: {...}) => Promise<void>
  "permission.ask"?: (input: Permission, output: {...}) => Promise<void>
  "tool.execute.before"?: (input: {...}, output: {...}) => Promise<void>
  "tool.execute.after"?: (input: {...}, output: {...}) => Promise<void>
  "experimental.chat.messages.transform"?: (input: {}, output: {...}) => Promise<void>
  "experimental.chat.system.transform"?: (input: {}, output: {...}) => Promise<void>
  "experimental.session.compacting"?: (input: {...}, output: {...}) => Promise<void>
  "experimental.text.complete"?: (input: {...}, output: {...}) => Promise<void>
}

// 插件加载
export async function trigger<Name extends keyof Required<Hooks>>(name: Name, input: Input, output: Output): Promise<Output> {
  if (!name) return output
  for (const hook of await state().then((x) => x.hooks)) {
    const fn = hook[name]
    if (!fn) continue
    await fn(input, output)
  }
  return output
}
本站教程完全免费,广告收入用于覆盖服务器成本。感谢您的支持!