在日常开发中,我们经常会遇到这样的场景:每次 AI 写完代码需要手动运行 lint 检查、想要在会话结束时自动提交代码、或者希望在特定文件变更时触发测试套件。如果你熟悉 Git Hooks 的概念,你就会明白"钩子"在开发工作流中的价值——它让你在特定事件发生时自动执行预设动作,无需手动干预。
OpenCode 作为一款现代化的 AI 编程助手,内置了一套强大且灵活的 Hooks 系统。通过 Hooks,你可以监听 OpenCode 的生命周期事件(会话创建、文件变更、工具调用前后等),并在这些事件发生时自动执行自定义逻辑。本文将深入讲解 OpenCode Hooks 系统的两种实现方式——插件 Hooks 和 YAML Hooks,并给出完整的实战示例。
OpenCode 的 Hooks 系统包含两条路径:
| 方式 | 描述 | 适用场景 |
|------|------|----------|
| 插件 Hooks | 通过 JavaScript/TypeScript 文件实现,使用 export 函数暴露钩子 | 复杂逻辑、需要访问上下文、自定义工具集成 |
| YAML Hooks | 通过 hooks.yaml 声明式配置,配合 opencode-yaml-hooks 插件使用 | 简单脚本执行、自动化工作流、CI 集成 |
两种方式可以同时使用,互不冲突。插件 Hooks 提供更细粒度的控制能力和完整的上下文访问,而 YAML Hooks 胜在简洁直观、易于维护。
在 OpenCode 中,插件就是一个放置在 .opencode/plugins/ 目录下的 JavaScript 或 TypeScript 文件。每个插件文件 export 一个或多个钩子函数,OpenCode 在启动时自动加载它们。钩子函数接收一个 context 对象,返回一个 hooks 对象。
插件加载的优先级如下:
全局配置 (~/.config/opencode/opencode.json) 中的 plugin 字段
项目配置 (opencode.json) 中的 plugin 字段
全局插件目录 (~/.config/opencode/plugins/)
项目插件目录 (.opencode/plugins/)
// .opencode/plugins/my-first-hook.ts
export const guard = {
name: "env-guard",
async toolExecuteBefore(context: any) {
const { toolName, input } = context
if (toolName === "read" && input?.filePath?.includes(".env")) {
return { prevented: true, message: "禁止读取环境配置文件" }
}
},
async event(context: any) {
const { event } = context.input
if (event.type === "session.created") {
console.log(`新会话已创建: ${new Date().toISOString()}`)
}
},
}
OpenCode 插件系统提供了丰富的钩子事件,以下是完整的钩子列表及其用途:
#### 配置与初始化钩子
| 钩子 | 触发时机 | 用途 |
|------|----------|------|
| config | 插件初始化时 | 动态注入命令、Agent、MCP 服务器 |
| tool | 插件初始化时 | 注册自定义工具 |
| auth | 认证需求触发时 | 注册自定义认证提供者 |
#### 消息与对话钩子
| 钩子 | 触发时机 | 可修改 |
|------|----------|--------|
| chat.message | 处理传入消息时 | 消息内容和部件 |
| chat.params | 发送 LLM 请求前 | temperature、topP 等参数 |
| experimental.text.complete | LLM 生成文本后 | 生成的文本内容 |
#### 工具执行钩子
| 钩子 | 触发时机 | 可修改 |
|------|----------|--------|
| tool.execute.before | 工具执行前 | 工具参数、阻止执行 |
| tool.execute.after | 工具执行后 | 工具输出 |
#### 事件监听钩子
event 钩子可以订阅所有系统事件,包括:
session.created、session.deleted、session.compacted、session.idlefile.edited、file.watcher.updatedtool.execute.before、tool.execute.aftermessage.part.removed、message.part.updated、message.removed、message.updatedpermission.replied、permission.updatedtui.prompt.append、tui.command.execute、tui.toast.showlsp.client.diagnostics、lsp.updatedcommand.executed#### 会话压缩钩子
| 钩子 | 触发时机 | 用途 |
|------|----------|--------|
| experimental.session.compacting | 会话压缩前 | 注入自定义上下文或替换压缩提示词 |
创建一个插件,在文件写入后自动运行 ESLint 检查:
// .opencode/plugins/code-quality.ts
import { execSync } from "child_process"
export const qualityGuard = {
name: "code-quality",
async toolExecuteAfter(context: any) {
const { toolName, output } = context
if (toolName !== "write" && toolName !== "edit") {
return
}
try {
const result = execSync("npx eslint --fix --quiet .", {
cwd: process.cwd(),
timeout: 30000,
})
console.log("[qualityGuard] ESLint 检查通过")
} catch (error: any) {
console.warn("[qualityGuard] ESLint 发现问题:", error.stdout?.toString())
}
},
}
记录每个会话的关键操作,便于回顾和审计:
// .opencode/plugins/session-logger.ts
import fs from "fs"
import path from "path"
export const sessionLogger = {
name: "session-logger",
async event(context: any) {
const { event } = context.input
const logFile = path.join(
process.cwd(),
".opencode",
"session-logs.jsonl"
)
const entry = {
timestamp: new Date().toISOString(),
type: event.type,
sessionId: event.session?.id,
details: event.type.startsWith("tool.") ? event.tool?.name : null,
}
fs.appendFileSync(logFile, JSON.stringify(entry) + "\n")
},
}
如果你不需要编写复杂的 JavaScript 逻辑,YAML Hooks 是一个更轻量的选择。它通过 YAML 配置文件定义钩子行为,使用了一个名为 opencode-yaml-hooks 的社区插件。
首先在 opencode.json 中注册插件:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-yaml-hooks"]
}
然后在 .opencode/hook/hooks.yaml 中定义钩子规则:
hooks:
- event: file.changed
conditions:
- matchesAnyPath: src/**/*.ts
actions:
- bash: "npm run lint -- --fix"
- event: session.created
scope: main
actions:
- bash: "echo '新会话开始于:' $(date)"
配置文件位置(按加载顺序):
| 平台 | 全局配置 | 项目配置 |
|------|----------|----------|
| macOS / Linux | ~/.config/opencode/hook/hooks.yaml | <project>/.opencode/hook/hooks.yaml |
| Windows | ~/.config/opencode/hook/hooks.yaml 优先,否则 %APPDATA%/opencode/hook/hooks.yaml | <project>/.opencode/hook/hooks.yaml |
hooks:
- id: my-hook-id # 可选:用于后续覆盖或禁用
event: <事件名> # 必填:触发事件
scope: all|main|child # 可选:作用域,默认 all
runIn: current|main # 可选:执行位置,默认 current
async: true|false # 可选:是否异步执行
action: stop # 可选:仅 tool.before.* 可用,阻止工具执行
conditions: # 可选:条件过滤
- matchesCodeFiles
- matchesAnyPath: src/**/*.ts
actions: # 必填:执行动作数组
- bash: "npm test"
#### 支持的事件类型
| 事件 | 触发时机 | 说明 |
|------|----------|------|
| session.created | 会话创建时 | 适合初始化操作 |
| session.deleted | 会话删除时 | 适合清理操作 |
| session.idle | 会话空闲时 | 适合自动提交等操作 |
| file.changed | 文件被修改后 | 最常用,适合 lint、format |
| tool.before.* | 任何工具执行前 | 适合安全检查、权限控制 |
| tool.before.<toolname> | 特定工具执行前 | 如 tool.before.write |
| tool.after.* | 任何工具执行后 | 适合日志记录 |
| tool.after.<toolname> | 特定工具执行后 | 如 tool.after.edit |
#### 动作类型
YAML Hooks 支持三种动作类型:
# 1. Bash 命令(最常用)
actions:
- bash: "npm run lint"
- bash:
command: "$OPENCODE_PROJECT_DIR/.opencode/hooks/init.sh"
timeout: 30000 # 毫秒,默认 60000
# 2. OpenCode 命令
actions:
- command: "/share"
# 3. 工具调用
actions:
- tool:
name: write
args:
filePath: "hook-output.txt"
content: "钩子已触发"
#### 条件过滤系统
# 仅在修改了代码文件时触发
conditions:
- matchesCodeFiles
# 仅在匹配特定路径时触发
conditions:
- matchesAnyPath: src/**/*.ts
# 要求所有文件都匹配指定路径
conditions:
- matchesAllPaths:
- package.json
- apps/*/package.json
多个条件之间是"与"关系,必须全部满足才会执行。
# .opencode/hook/hooks.yaml
hooks:
- id: auto-format
event: file.changed
conditions:
- matchesAnyPath:
- src/**/*.ts
- src/**/*.tsx
- src/**/*.js
actions:
- bash: "npx prettier --write src/**/*.{ts,tsx,js}"
- bash: "npx eslint --fix src/**/*.{ts,tsx,js}"
hooks:
- id: block-env-write
event: tool.before.write
conditions:
- matchesAnyPath:
- .env
- .env.local
- .env.production
- **/.env
action: stop
actions:
- bash: "echo '❌ 禁止覆盖环境文件!'"
action: stop 会让 OpenCode 尝试中止当前会话,有效阻止危险操作。
hooks:
- id: auto-test-on-idle
event: session.idle
scope: main
conditions:
- matchesCodeFiles
async: true
actions:
- bash:
command: "npm test -- --changed"
timeout: 120000
async: true 表示异步执行,不会阻塞 OpenCode 的正常工作流。注意异步钩子只能使用 bash 动作。
了解钩子的触发顺序对于正确设计工作流至关重要。以 write 工具为例,完整的触发链路如下:
1. tool.before.* (所有工具之前) 2. tool.before.write (特定工具之前) 3. 工具执行 4. file.changed (检测到文件变更) 5. tool.after.* (所有工具之后) 6. tool.after.write (特定工具之后)
全局钩子先于项目钩子加载,同一事件内的钩子按声明顺序执行。
YAML Hooks 支持通过 id 字段进行覆盖操作。这在你想在项目级配置中修改全局钩子行为时特别有用:
# 全局配置中定义
hooks:
- id: gbl-auto-lint
event: file.changed
actions:
- bash: "npm run lint"
# 项目配置中覆盖
hooks:
- id: gbl-auto-lint
override:
event: file.changed
actions:
- bash: "npm run lint:strict"
# 项目中禁用
hooks:
- id: gbl-auto-lint
disable: true
覆盖规则:
file.changed对于文件相关的自动化操作(如 lint、format、测试),优先使用 file.changed 事件而不是 tool.after.*。file.changed 在文件实际变更后才触发,语义更清晰。
# 推荐
- event: file.changed
conditions:
- matchesAnyPath: src/**/*.ts
actions:
- bash: "npm run lint"
# 不推荐 —— 语义不够精确
- event: tool.after.*
conditions:
- matchesAnyPath: src/**/*.ts
actions:
- bash: "npm run lint"
对于可能耗时较长的操作(如运行完整测试套件),使用 async: true 避免阻塞交互:
- event: session.idle
scope: main
async: true
actions:
- bash:
command: "npm run test:e2e"
timeout: 300000
scope 限制触发范围scope 可选值为 all(默认)、main、child。main 限制仅在主会话触发,child 限制仅在子 Agent 会话触发。对于全局性操作(如自动 commit),建议使用 scope: main。
插件钩子中的同步或长时间运行的操作会阻塞 OpenCode 的正常工作。对于重操作,考虑:
复杂项目建议组合使用两种方式:
OpenCode 的 Hooks 系统为你提供了对 AI 编程助手行为前所未有的控制力。通过合理利用 Hooks,你能够构建一个高度自动化的开发环境:
用好 Hooks 的关键在于理解事件的触发时机和执行顺序,选择合适的钩子类型,并在全局和项目级别之间做好平衡。当你的 OpenCode 开始自动运行 lint 检查、阻止危险操作、在会话空闲时触发测试时,你会发现:好的工具不是替你思考,而是帮你省下重复思考的时间。