本文件以本專案 opencode-git-tools 為範例,說明如何從零建立 OpenCode Plugin + 自訂 Tools + Slash Commands,讓 AI 優先使用你的工具,而不是直接跑 bash。
官方文件:
OpenCode 支援兩種路徑,用途不同:
| 方式 | 放置位置 | 適合情境 |
|---|---|---|
| 獨立 Tool 檔 | .opencode/tools/ 或 ~/.config/opencode/tools/ |
單一功能、一檔一 tool、不需 hook |
| Plugin 內嵌 Tools | .opencode/plugins/ 或 ~/.config/opencode/plugins/ |
多個相關 tool、需要 hook、要引導 AI 行為 |
本專案採用 Plugin 內嵌 Tools,因為 Git 操作需要:
- 一次註冊多個相關 tool(
gitStatus、gitCommit…) - 用 hook 自動注入「請用 plugin tool,別用 bash git」
- 在 context compaction 後仍保留 Git 工作流程提示
- 搭配 slash commands(
/git-commit)形成固定流程
opencode-git-tools/
├── src/
│ └── index.ts # Plugin 主檔:tools + hooks
├── commands/ # Slash commands(安裝時複製到全域)
│ ├── git-status.md
│ ├── git-commit.md
│ ├── git-review.md
│ └── git-tree.md
├── hooks/ # 可選:專案 git pre-commit hook
│ ├── pre-commit
│ └── pre-commit.ts
├── scripts/
│ ├── install-global.mjs # 安裝 plugin + commands 到 ~/.config/opencode
│ └── install-git-hooks.mjs # 安裝 git hook 到目前專案
├── install.ps1 / install.sh # 一鍵安裝入口
├── opencode.json.example # 設定範例
└── package.json
安裝後,OpenCode 實際載入位置:
~/.config/opencode/
├── plugins/
│ └── opencode-git-tools.ts # 從 src/index.ts 複製
├── commands/
│ ├── git-status.md
│ ├── git-commit.md
│ └── ...
├── package.json # 含 @opencode-ai/plugin 依賴
├── node_modules/
└── opencode.jsonc # plugin 路徑註冊
在寫程式前,先回答:
- 要封裝哪些操作?(例:status、diff、commit)
- AI 目前用 bash 會遇到什麼問題?(例:終端機輸出太亂、CRLF 警告洗版)
- 要不要固定工作流程?(例:status → diff → review → commit)
- 要不要 slash command?(例:
/git-commit) - 安裝範圍? 全域(所有專案)或專案內(
.opencode/)
mkdir my-opencode-plugin && cd my-opencode-plugin
npm init -ypackage.json 最小設定:
{
"name": "my-opencode-plugin",
"version": "1.0.0",
"type": "module",
"main": "src/index.ts",
"peerDependencies": {
"@opencode-ai/plugin": ">=1.0.0"
}
}建立 src/index.ts:
import { type Plugin, tool } from "@opencode-ai/plugin";
export const MyPlugin: Plugin = async ({ client, directory, $ }) => {
// 啟動時寫 log(不要用 console.log)
await client.app.log({
body: {
service: "my-plugin",
level: "info",
message: `Plugin loaded in ${directory}`,
},
});
return {
// hooks 放這裡
tool: {
// 自訂 tools 放這裡
},
};
};
export default MyPlugin;Plugin 函式收到的 context:
| 參數 | 用途 |
|---|---|
directory |
目前工作目錄 |
worktree |
Git worktree 根目錄 |
client |
OpenCode SDK(log、API 互動) |
$ |
Bun Shell API,執行外部指令 |
project |
專案資訊 |
使用 tool() helper,參數用 Zod schema(tool.schema):
myStatus: tool({
description: "Get project status (prefer over bash)",
args: {
verbose: tool.schema.boolean().optional().default(false),
},
async execute(args) {
// 用 $ 執行 shell,回傳字串給 AI
const flags = args.verbose ? "-v" : "";
return (await $`git -C ${directory} status ${flags}`.text()).trim();
},
}),Tool 設計原則:
description要寫「何時用、優於 bash 的原因」- 參數用
.describe()補充語意 execute回傳 精簡、可讀的字串,不要把整段 raw terminal 輸出原樣丟回- 需要長文字輸入時(如 commit message),寫入暫存檔,不要塞進
-m "..."
Plugin 的 tool 物件可放多個 tool:
return {
tool: {
gitStatus: tool({ /* ... */ }),
gitDiff: tool({ /* ... */ }),
gitCommit: tool({ /* ... */ }),
},
};AI 呼叫時使用 tool 名稱:gitStatus、gitCommit 等。
僅註冊 tool 不夠,模型仍可能跑 bash git ...。需要 hook 主動提醒。
config: async (config) => {
config.instructions = config.instructions ?? [];
config.instructions.push(
"my-plugin: prefer myPlugin* tools over raw bash",
);
},在對話開始時,把工具使用說明插到第一則 user message 前面:
"experimental.chat.messages.transform": async (_input, output) => {
const firstUser = output.messages.find((m) => m.info.role === "user");
if (!firstUser?.parts.length) return;
// 避免重複注入
if (firstUser.parts.some((p) => p.type === "text" && p.text.includes("<MY_PLUGIN>"))) return;
const ref = firstUser.parts[0];
firstUser.parts.unshift({
...ref,
type: "text",
text: `<MY_PLUGIN>
Use myStatus, myCommit instead of bash.
</MY_PLUGIN>`,
});
},Session 被 compact 後,預設 prompt 可能遺失工具提示。用此 hook 補回:
"experimental.session.compacting": async (_input, output) => {
output.context.push(`
## My Plugin
Prefer: myStatus, myCommit. Never bash equivalent.
`);
},| Hook | 用途 |
|---|---|
tool.execute.before |
攔截 / 修改 tool 執行前的參數 |
tool.execute.after |
執行後處理結果 |
shell.env |
注入環境變數到所有 shell |
event |
訂閱 session.idle 等事件 |
範例:禁止讀 .env
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath?.includes(".env")) {
throw new Error("Do not read .env files");
}
},在 commands/ 放 markdown,檔名即指令名:
commands/my-commit.md:
---
description: Stage and commit using plugin tools
agent: build
---
Follow this workflow using plugin tools only:
1. `myStatus` — check changes
2. `myDiff` staged:false — review diff
3. `myCommit` with conventional message
If $ARGUMENTS is provided, use it as the commit subject.使用方式:在 TUI 輸入 /my-commit fix login bug。
Frontmatter 常用欄位:
| 欄位 | 說明 |
|---|---|
description |
TUI 中顯示的說明 |
agent |
指定 agent(如 build) |
model |
覆寫模型 |
subtask |
是否強制 subagent |
Prompt 進階語法:
$ARGUMENTS— 使用者輸入的全部參數$1,$2— 位置參數@path/to/file— 嵌入檔案內容!`git log -5`— 嵌入 shell 輸出
參考 scripts/install-global.mjs,安裝時要做四件事:
- 複製
src/index.ts→~/.config/opencode/plugins/<name>.ts - 複製
commands/*.md→~/.config/opencode/commands/ - 確保
~/.config/opencode/package.json有@opencode-ai/plugin - 在
opencode.jsonc的plugin陣列註冊路徑
Windows 路徑需轉成 OpenCode 可讀格式(本專案 toConfigPath() 會處理)。
安裝後 必須重啟 OpenCode。
# CLI 驗證 tool 是否可用
opencode run "call gitStatus and show the result"
# TUI 驗證 slash command
/git-status確認 AI 日誌中出現 plugin 載入訊息,且實際呼叫的是 gitStatus 而非 bash git status。
問題(見專案截圖案例):
- AI 跑
git commit -m "超長多行訊息..."會把整段文字印在終端機 - 與 AI Thought 輸出、CRLF 警告混在一起,畫面難以閱讀
解法(gitCommit tool):
const msgPath = join(tmpdir(), `oc-git-commit-${Date.now()}.txt`);
await writeFile(msgPath, message, "utf8");
await $`git -c advice.convertCRLF=false -C ${directory} commit -F ${msgPath} -q`;然後只回傳精簡摘要:
Committed abc1234 on main
refactor(codec): unify CausalConvNet
3 files changed, 45 insertions(+), 12 deletions(-)
Windows 上常見 CRLF 警告:
warning: in the working copy of 'README.md', LF will be replaced by CRLF...
處理方式:
- 源頭抑制:
-c advice.convertCRLF=false - 輸出過濾:
sanitizeGitOutput()移除 warning 行
注意:在 Bun/OpenCode 的
$template 中,不要把"-c advice.convertCRLF=false"做成一個字串再插值。插值會被視為單一 argv,Git 會報unknown option: -c advice.convertCRLF=false。請保持git -c ${configValue} ...或git -c advice.convertCRLF=false ...的 token 順序。
function sanitizeGitOutput(text: string): string {
return text
.split("\n")
.filter((line) => !/^warning: in the working copy of /i.test(line))
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}| 層級 | 機制 | 作用時機 |
|---|---|---|
| 1 | config.instructions |
每次 session 全域 |
| 2 | experimental.chat.messages.transform |
第一則 user message |
| 3 | experimental.session.compacting |
Context 被壓縮後 |
| 4 | Slash commands | 使用者明確觸發流程 |
| Tool | 取代的 bash | 說明 |
|---|---|---|
gitStatus |
git status |
過濾 CRLF 警告 |
gitDiff |
git diff |
支援 staged / ref |
gitLog |
git log |
精簡歷史 |
gitTree |
git log --graph |
分支拓撲 |
gitBranch |
git branch/checkout |
list / create / switch |
gitCommit |
git add && git commit |
安靜提交 + 精簡回傳 |
gitStash |
git stash |
push / pop / list / drop |
gitPrecommitReview |
手動看 diff | 提交前審查 |
建議工作流程:
gitStatus → gitDiff(staged:true) → gitPrecommitReview → gitCommit
tool.schema 即 Zod,常用型別:
args: {
// 必填字串
message: tool.schema.string().describe("Commit message"),
// 可選布林,有預設值
staged: tool.schema.boolean().optional().default(false),
// 數字
count: tool.schema.number().optional().default(10),
// 列舉
action: tool.schema.enum(["list", "create", "delete"]).default("list"),
// 字串陣列
files: tool.schema.array(tool.schema.string()).optional(),
}// 指定 git 工作目錄
await $`git -C ${directory} status`.text();
// 帶 git config 旗標(抑制警告)
await $`git -c advice.convertCRLF=false -C ${directory} add -A`;
// 安靜模式
await $`git -C ${directory} commit -F ${msgPath} -q`;
// 取 stdout 文字
const output = (await $`git -C ${directory} diff --cached`.text()).trim();避免:
- 把超長文字直接放進 template literal 當
-m參數 - 用
""當 optional flag/path 的佔位參數;請分支組合命令 - 未過濾空字串就把 file path array 傳給
git add - 回傳未處理的 stderr 警告
- 在 plugin 裡用
console.log(改用client.app.log)
powershell -ExecutionPolicy Bypass -File .\install.ps1- 所有專案都能用
- 適合 Git 這類通用工具
把檔案放到專案的 .opencode/:
your-project/
└── .opencode/
├── package.json # 需含 @opencode-ai/plugin
├── plugins/
│ └── my-plugin.ts
└── commands/
└── my-command.md
- 只在本專案生效
- 適合專案特定工具(內部 API、部署腳本等)
opencode.jsonc:
OpenCode 啟動時會用 Bun 自動安裝 npm plugin。
本專案提供 pre-commit hook,在 git commit 前呼叫 OpenCode tool:
hooks/pre-commit:
opencode run gitPrecommitReview || echo "AI Review skipped"安裝到目前專案:
node scripts/install-git-hooks.mjs
# 或
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Hooks這讓 人類手動 commit 時也能觸發 AI 審查;與 plugin tool 互補。
新增一個 tool 時,依序確認:
-
description說明用途,並註明「優先於 bash」 -
args每個欄位有.describe() -
execute回傳精簡、結構化文字 - 長輸入用檔案 / API,不塞進 shell 一行指令
- 外部指令錯誤有被 try/catch 或向上拋出
- 輸出經過雜訊過濾(如 CRLF warning)
-
GIT_TOOLS_GUIDANCE/ compaction context 有提到新 tool - 視需要新增
commands/xxx.md - 重新安裝 plugin 並重啟 OpenCode
- 用
opencode run或 slash command 驗證
- 確認 plugin 已安裝且出現在
opencode.jsonc - 重啟 OpenCode
- 加強
experimental.chat.messages.transform指引 - 用 slash command 明確指定流程
- 在
description寫「prefer over bash」
檢查:
# 依賴是否存在
ls ~/.config/opencode/node_modules/@opencode-ai/plugin
# 手動安裝
npm install --prefix ~/.config/opencode @opencode-ai/pluginopencode.jsonc 的 plugin 路徑建議用 toConfigPath() 轉換後寫入,或用手動格式:
/c/Users/you/.config/opencode/plugins/opencode-git-tools.ts
可以。Plugin tools 和獨立 tool 檔都會載入。Plugin 適合多 tool + hook;獨立檔適合單一簡單功能。
- 用 plugin tool 取代 bash
- 外部指令加
-q(quiet) - 過濾 warning 行
- 回傳摘要而非 raw output
- Commit message 用
-F檔案,不用-m長字串
複製以下檔案即可開始一個新 plugin:
src/index.ts
import { type Plugin, tool } from "@opencode-ai/plugin";
const GUIDANCE = `<DEMO_PLUGIN>
You have demoStatus and demoEcho tools. Do not use bash equivalents.
</DEMO_PLUGIN>`;
export const DemoPlugin: Plugin = async ({ client, directory, $ }) => {
await client.app.log({
body: { service: "demo-plugin", level: "info", message: "loaded" },
});
return {
config: async (config) => {
config.instructions = config.instructions ?? [];
config.instructions.push("demo-plugin: use demo* tools");
},
"experimental.chat.messages.transform": async (_input, output) => {
const firstUser = output.messages.find((m) => m.info.role === "user");
if (!firstUser?.parts.length) return;
if (firstUser.parts.some((p) => p.type === "text" && p.text.includes("<DEMO_PLUGIN>"))) return;
const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: "text", text: GUIDANCE });
},
tool: {
demoEcho: tool({
description: "Echo text (prefer over bash echo)",
args: {
text: tool.schema.string().describe("Text to echo"),
},
async execute(args) {
return `Echo: ${args.text}`;
},
}),
demoStatus: tool({
description: "Show current directory",
args: {},
async execute() {
return `Working directory: ${directory}`;
},
}),
},
};
};
export default DemoPlugin;commands/demo-echo.md
---
description: Echo text using demoEcho tool
agent: build
---
Call `demoEcho` with text: $ARGUMENTS
Do not use bash echo.- 本專案原始碼:
src/index.ts - 安裝腳本:
scripts/install-global.mjs - 設定範例:
opencode.json.example - Slash command 範例:
commands/git-commit.md
若要擴充本專案,建議流程:
- 在
src/index.ts的tool區塊新增 tool - 更新
GIT_TOOLS_GUIDANCE與 compaction context - 視需要新增
commands/*.md - 執行
install.ps1或install.sh - 重啟 OpenCode 並驗證
{ "plugin": ["my-opencode-plugin"] }