创建自定义工具
在 OpenCode 里给模型提供可控、可复用的项目动作。
Custom tools 是你写给 LLM 调用的函数。它们和 OpenCode 内置的 read、write、bash、grep 等工具并列存在,适合把项目专有、重复出现、输入输出稳定的动作封装成可控入口。
这一篇用 12 分钟换什么:你会知道什么时候值得写 custom tool、工具文件放哪里、工具名怎么生成、多个工具怎么导出、参数 schema 怎么写、context 能拿到什么,以及哪些安全边界不能交给模型猜。
先判断是否真的需要
不要把 custom tool 当成“更高级的 bash”。先按这条路径判断:
flowchart LR
Need["一个动作反复出现"] --> Builtin{"内置工具能解决?"}
Builtin -->|能| UseBuiltin["用 read / grep / bash / edit"]
Builtin -->|不能| External{"是外部通用系统?"}
External -->|是| MCP["优先 MCP"]
External -->|不是| Stable{"输入输出稳定?"}
Stable -->|否| Prompt["继续用提示词或脚本"]
Stable -->|是| Custom["封装 custom tool"]
style UseBuiltin fill:#dcfce7,stroke:#22c55e
style MCP fill:#dbeafe,stroke:#3b82f6
style Custom fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
适合封装的动作:
- 查询内部服务状态。
- 读取项目专有配置格式。
- 运行固定诊断脚本并返回摘要。
- 调用公司内部 API 的只读接口。
- 把一段稳定 shell / Python / Node 脚本包装成结构化工具。
不适合封装的动作:一次性命令、危险删除、生产发布、数据库写入、万能 shell wrapper、会返回大量日志的脚本。
工具一旦进入 OpenCode,就会成为模型可能调用的能力。能用 permission 控制内置工具,就不要靠覆盖内置工具名来“限制”它。
1. 工具放在哪里
工具定义必须是 TypeScript 或 JavaScript 文件,但工具定义内部可以调用任何语言写的脚本。
| 位置 | 适合什么 |
|---|---|
.opencode/tools/ | 项目级工具,只服务当前仓库,推荐从这里开始 |
~/.config/opencode/tools/ | 全局工具,影响所有项目,确认稳定后再放这里 |
大多数项目先用 .opencode/tools/。这样工具随仓库一起演进,不会把一个项目的假设带到其他项目。
2. 最小工具结构
最简单的方式是使用 tool() helper,它提供类型安全和参数校验。
import { tool } from "@opencode-ai/plugin";
export default tool({
description: "Return current project directory information",
args: {},
async execute(args, context) {
return `directory=${context.directory}\nworktree=${context.worktree}`;
},
});文件名会变成工具名。上面这个文件会创建 project-info 工具。新手先从只读工具开始,确认调用链、输出格式和权限边界都稳定,再考虑写入或外部请求。
3. 一个文件导出多个工具
官方文档说明,一个文件可以导出多个工具。每个 export 会变成独立工具,名字是 <filename>_<exportname>。
例如 .opencode/tools/math.ts 里导出 add 和 multiply,OpenCode 会注册成 math_add 和 math_multiply。
如果工具共享同一组内部 helper,放在一个文件里更容易维护;如果职责不同,拆成多个文件更清楚。不要为了少建文件把无关工具塞到一起。
4. 参数 schema 要写给模型看
tool.schema 就是 Zod。参数越少、描述越具体,模型越不容易误用。
args: {
query: tool.schema.string().describe("Read-only SQL query to execute"),
}也可以直接导入 Zod,返回普通对象:
import { z } from "zod";
args: {
param: z.string().describe("Parameter description"),
}好的参数描述会写清边界,例如“仓库内相对路径”“只读 SQL”“不包含密钥”“只允许 staging 环境”。不要只写 query、path、name 这种模型无法判断风险的描述。
5. context 能拿到什么
工具会收到当前 session(会话)的上下文。官方示例里包括 agent、sessionID、messageID、directory 和 worktree。
context.directory是 session 工作目录(用户启动 OpenCode 时所在的那个文件夹)。context.worktree是 Git worktree(工作树,简单理解为"这个仓库当前 checkout 的文件树根目录"——一个 Git 仓库可以同时有多个 worktree,每个对应一个分支)的根目录。
路径拼接优先基于 context.worktree 或 context.directory,不要假设用户总在项目根目录启动 OpenCode。
6. 调用 Python 或 Shell 脚本
工具定义必须是 TypeScript / JavaScript,但真实逻辑可以放到 Python、Shell 或其他语言脚本里。
核心模式是:工具定义负责 schema、路径和输出摘要,脚本负责实际业务逻辑。例如用 context.worktree 找到项目内脚本,再通过 Bun shell 调用:
const script = path.join(context.worktree, ".opencode/tools/add.py");
const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text();
return result.trim();这类结构适合复用已有脚本。脚本本身仍要能在终端独立运行,不要只在 OpenCode 对话里才“看起来能跑”。
7. 工具名冲突会覆盖内置工具
Custom tools 按工具名注册。如果自定义工具和内置工具同名,自定义工具会优先。
例如 .opencode/tools/bash.ts 会替换内置 bash。这不是普通命名问题,而是会改变模型可调用的基础能力。
除非你明确要替换内置工具,否则不要使用 bash、read、write、edit 这类名字。如果只是想禁用或收紧内置工具,应该用 permissions。
8. 安全边界
设计 custom tool 时,默认按“会被模型频繁调用,也可能被错误参数调用”处理:
- 默认只读。
- 参数必须校验,不把模型输入直接拼 shell。
- 输出要短,只返回下一步判断需要的信息。
- 密钥从环境变量或凭据系统读取,不写进工具文件。
- 写入、删除、发布、数据库操作必须有 dry-run、确认和权限边界。
- 错误返回清楚原因,不把完整堆栈和敏感环境打进上下文。
9. 验收清单
一个可交付的 custom tool 至少满足:
- 模型能从
description判断什么时候调用它。 - 参数 schema 足够具体,错误参数会被拒绝。
- 工具名不和内置工具冲突,除非有明确替换意图。
- 同一输入多次运行结果稳定。
- 输出短、可读、无密钥。
- 项目级工具优先,确认稳定后再考虑全局化。
接下来去哪
工具总览
先理解 custom tool 和内置工具、MCP、permission 的边界。
MCP 服务器
如果需求是外部系统上下文,MCP 往往比自写工具更合适。
Plugin
只有需要改变 OpenCode 生命周期或注册更深扩展时,才考虑 plugin。
权限
写入、shell、外部系统动作要靠 permission 管住。
官方资料
- OpenCode Custom Tools:https://opencode.ai/docs/custom-tools
- OpenCode Tools:https://opencode.ai/docs/tools
- OpenCode Permissions:https://opencode.ai/docs/permissions
- OpenCode Plugins:https://opencode.ai/docs/plugins