plugins.mdx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. ---
  2. title: "Plugins"
  3. ---
  4. Plugins extend OpenCode in-process. They can transform agents, models, commands,
  5. integrations, references, skills, and tools; intercept model requests and tool
  6. execution; and call a subset of the V2 client.
  7. <Callout type="warning">
  8. The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration may change before the stable release.
  9. Use the `/v2` exports described on this page.
  10. </Callout>
  11. ## Load plugins
  12. Plugins can be loaded from npm packages, explicit local paths, or config
  13. directories. Each module must have one default export containing a unique
  14. plugin `id` and a `setup` function.
  15. ### Configuration
  16. Add ordered entries to the `plugins` field in `opencode.json(c)`:
  17. ```jsonc title="opencode.jsonc"
  18. {
  19. "$schema": "https://opencode.ai/config.json",
  20. "plugins": [
  21. "opencode-acme-plugin@1.2.0",
  22. "@acme/opencode-plugin",
  23. "./plugins/local.ts",
  24. {
  25. "package": "./plugins/reviewer.ts",
  26. "options": {
  27. "agent": "reviewer",
  28. "strict": true,
  29. },
  30. },
  31. ],
  32. }
  33. ```
  34. A string is either a package specifier or a local path. Local paths must start
  35. with `./` or `../` and resolve relative to the configuration file containing
  36. the entry. Absolute paths and `file://` URLs are also supported. Both scoped
  37. packages and versioned package specifiers are supported.
  38. Use the object form to pass JSON configuration to the plugin. OpenCode passes
  39. `options` unchanged as `ctx.options`; omitted options become an empty object.
  40. The plugin owns validation and defaults for its options.
  41. See [Config](/config#locations) for configuration locations and precedence.
  42. Entries from all applicable files are processed from lowest to highest
  43. precedence rather than replacing the entire array.
  44. ### Local discovery
  45. OpenCode automatically scans this directory in every discovered OpenCode config
  46. directory:
  47. ```text
  48. .opencode/plugins/
  49. ```
  50. The equivalent global directory is `~/.config/opencode/plugins/`. Direct `.ts`
  51. and `.js` children are loaded. An immediate child directory is also loaded as a
  52. package when OpenCode can resolve a string `exports`, `module`, or `main`
  53. entrypoint, or an `index.ts` or `index.js` file.
  54. A `plugins/` directory beside a project-root `opencode.json` is not discovered
  55. automatically. Put it under `.opencode/`, or add its file explicitly with a
  56. relative config entry.
  57. ### Enable and disable
  58. A string beginning with `-` disables plugins by their exported `id`. `*`
  59. matches every ID, and a suffix of `.*` matches an ID prefix. Directives are
  60. applied in order:
  61. ```jsonc title="opencode.jsonc"
  62. {
  63. "plugins": ["./plugins/reviewer.ts", "-acme.reviewer", "-opencode.provider.*", "opencode.provider.openai"],
  64. }
  65. ```
  66. Package specifiers and local paths locate plugin modules; they are not disable
  67. selectors. Use the `id` from the plugin's default export to disable it. A later
  68. ID entry re-enables a loaded or built-in plugin. Explicit config directives run
  69. after local auto-discovery, so they can disable discovered plugins by ID.
  70. User plugins are activated in configured order between OpenCode's internal
  71. plugin phases. Hooks run sequentially in registration order, and later hooks
  72. observe earlier mutations. Do not depend on the internal phase ordering while
  73. the API is beta.
  74. ### Installation and dependencies
  75. OpenCode installs bare package entries and their production dependencies into
  76. an isolated cache. Package installation does not run lifecycle scripts.
  77. Published packages should expose their plugin entrypoint and include every
  78. runtime import in `dependencies`.
  79. Local files and local package directories are imported directly. OpenCode does
  80. **not** install their dependencies. Install dependencies in a `package.json`
  81. visible from the plugin file, for example:
  82. ```sh
  83. cd .opencode
  84. bun add @opencode-ai/plugin@beta
  85. ```
  86. Match the plugin package version to the OpenCode release you target.
  87. Configuration and discovered plugin files under watched config directories are
  88. reloaded when they change. Reloading replaces the active plugin generation and
  89. releases its scoped registrations. Restart OpenCode after changing an npm
  90. package version or a local dependency when no watched file changed.
  91. ## Create a plugin
  92. Export the result of `Plugin.define` as the module default:
  93. ```ts title=".opencode/plugins/reviewer.ts"
  94. import { Plugin } from "@opencode-ai/plugin"
  95. export default Plugin.define({
  96. id: "acme.reviewer",
  97. setup: async (ctx) => {
  98. const description =
  99. typeof ctx.options.description === "string" ? ctx.options.description : "Reviews code for regressions"
  100. await ctx.agent.transform((agents) => {
  101. agents.update("reviewer", (agent) => {
  102. agent.description = description
  103. agent.mode = "subagent"
  104. })
  105. })
  106. },
  107. })
  108. ```
  109. `setup` runs each time the plugin is activated. Register long-lived behavior
  110. during setup; do not wait there on an infinite event stream. It may return a
  111. synchronous or asynchronous cleanup function. OpenCode awaits that cleanup
  112. when the plugin is disabled, reloaded, or shut down:
  113. ```ts
  114. setup: async (ctx) => {
  115. const controller = new AbortController()
  116. const task = synchronize(ctx, controller.signal)
  117. return async () => {
  118. controller.abort()
  119. await task
  120. }
  121. }
  122. ```
  123. Hook registrations are released automatically with the same plugin scope. Use
  124. the returned cleanup for resources the plugin owns, such as timers, watchers,
  125. connections, and background tasks.
  126. ### Context
  127. The plugin context is essentially an [OpenCode server client](/build/client).
  128. Its read and action methods use the same inputs and responses as the client. It
  129. adds plugin-only methods for transforms, runtime hooks, reloads, registrations,
  130. and plugin options.
  131. | Capability | Available operations |
  132. | ---------------------- | -------------------------------------------------------------------------------------------- |
  133. | `ctx.agent` | `list`, `get`, `transform`, `reload` |
  134. | `ctx.catalog.provider` | `list`, `get` |
  135. | `ctx.catalog.model` | `list`, `get`, `default` |
  136. | `ctx.catalog` | `transform`, `reload` |
  137. | `ctx.command` | `list`, `transform`, `reload` |
  138. | `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution |
  139. | `ctx.plugin` | `list` currently active plugin IDs |
  140. | `ctx.reference` | `list`, `transform`, `reload` |
  141. | `ctx.session` | `create`, `get`, `prompt`, `command`, `rename`, `synthetic`, `interrupt`, `wait`, and `hook` |
  142. | `ctx.skill` | `list`, `transform`, `reload` |
  143. | `ctx.tool` | `transform` and `hook` |
  144. | `ctx.aisdk` | `hook` |
  145. | `ctx.event` | `subscribe` to the current public server event stream |
  146. | `ctx.options` | Readonly options from the matching config object |
  147. ### Transform hooks
  148. Transform hooks let a plugin modify how OpenCode is configured. Use them to add
  149. or remove definitions, override settings, choose defaults, and provide tools or
  150. other sources.
  151. | Transform | Draft operations |
  152. | ----------------------- | ------------------------------------------------------------------------------------------------------- |
  153. | `agent.transform` | `list`, `get`, `default`, `update`, `remove` |
  154. | `catalog.transform` | Provider `list`, `get`, `update`, `remove`; model `get`, `update`, `remove`; default model `get`, `set` |
  155. | `command.transform` | `list`, `get`, `update`, `remove` |
  156. | `integration.transform` | Integration `list`, `get`, `update`, `remove`; method `list`, `update`, `remove` |
  157. | `reference.transform` | `add`, `remove`, `list` |
  158. | `skill.transform` | `source`, `list` |
  159. | `tool.transform` | `add` |
  160. Here's an example that keeps models synced from a remote source:
  161. ```js title=".opencode/plugins/remote-models.js"
  162. import { Plugin } from "@opencode-ai/plugin"
  163. export default Plugin.define({
  164. id: "acme.remote-models",
  165. setup: async (ctx) => {
  166. let models = []
  167. await ctx.catalog.transform((catalog) => {
  168. for (const model of models) {
  169. catalog.model.update(model.providerID, model.id, (draft) => Object.assign(draft, model))
  170. }
  171. })
  172. const refresh = async () => {
  173. const response = await fetch("https://example.com/opencode/models.json", {
  174. signal: AbortSignal.timeout(10_000),
  175. })
  176. if (!response.ok) return
  177. models = await response.json()
  178. await ctx.catalog.reload()
  179. }
  180. await refresh()
  181. const timer = setInterval(() => void refresh().catch(console.error), 60_000)
  182. return () => clearInterval(timer)
  183. },
  184. })
  185. ```
  186. `ctx.catalog.reload()` replays every catalog transform to derive the new
  187. catalog. Each plugin's logic remains composed with the others, so a later
  188. plugin can still modify models added by an earlier one. The catalog updates
  189. without restarting OpenCode.
  190. ### Runtime hooks
  191. Runtime hooks intercept live operations:
  192. | Hook | Mutable fields |
  193. | --------------------------------------------- | ------------------------------------------------------------------------------ |
  194. | `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
  195. | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
  196. | `ctx.session.hook("context", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
  197. | `ctx.session.hook("http.request", callback)` | `request`, immediately before provider dispatch |
  198. | `ctx.session.hook("http.response", callback)` | `response`, immediately after the provider responds |
  199. | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
  200. | `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure |
  201. HTTP hooks can modify requests and responses. They apply to native models; AI
  202. SDK models do not currently pass through these hooks. Request and response
  203. bodies are one-shot streams. Use `clone()` when you intentionally need a
  204. separate reader, but be aware that its slower branch may buffer data. To inspect
  205. or modify chunks while preserving streaming, replace the body with one piped
  206. through a `TransformStream`.
  207. ```ts
  208. await ctx.session.hook("http.request", (event) => {
  209. event.request.headers.set("x-session-id", event.sessionID)
  210. })
  211. await ctx.session.hook("http.response", (event) => {
  212. event.response = new Response(event.response.body, {
  213. status: event.response.status,
  214. headers: { ...Object.fromEntries(event.response.headers), "x-plugin": "enabled" },
  215. })
  216. })
  217. ```
  218. For example, remove a tool from selected model requests and normalize another
  219. tool's input:
  220. ```ts title=".opencode/plugins/guards.ts"
  221. import { Plugin } from "@opencode-ai/plugin"
  222. export default Plugin.define({
  223. id: "acme.guards",
  224. setup: async (ctx) => {
  225. await ctx.session.hook("context", (event) => {
  226. delete event.tools.write
  227. })
  228. await ctx.tool.hook("execute.before", (event) => {
  229. if (event.tool !== "lookup" || typeof event.input !== "object" || event.input === null) return
  230. event.input = { ...event.input, source: "plugin" }
  231. })
  232. },
  233. })
  234. ```
  235. A hook failure fails the operation it intercepts. Keep runtime hooks fast and
  236. handle expected errors inside the callback.
  237. ## Examples
  238. ### Add a tool
  239. Register a structural tool definition with a name and registration options.
  240. Define its input with JSON Schema and use an async executor:
  241. ```js title=".opencode/plugins/greeting.js"
  242. import { Plugin } from "@opencode-ai/plugin"
  243. export default Plugin.define({
  244. id: "acme.greeting",
  245. setup: async (ctx) => {
  246. await ctx.tool.transform((tools) => {
  247. tools.add("greeting", {
  248. description: "Create a greeting",
  249. input: {
  250. type: "object",
  251. properties: {
  252. name: { type: "string" },
  253. },
  254. required: ["name"],
  255. additionalProperties: false,
  256. },
  257. output: {
  258. type: "object",
  259. properties: { greeting: { type: "string" } },
  260. required: ["greeting"],
  261. additionalProperties: false,
  262. },
  263. execute: async ({ name }) => {
  264. const text = `Hello, ${name}!`
  265. return {
  266. output: { greeting: text },
  267. content: text,
  268. }
  269. },
  270. })
  271. })
  272. },
  273. })
  274. ```
  275. Unsupported characters in tool names are normalized to underscores. Namespace
  276. segments must begin with a letter, contain at most 64 letters, digits,
  277. underscores, or hyphens, and are joined with dots. Pass the optional third
  278. argument to `tools.add` to configure the registration with
  279. `{ namespace, codemode }`:
  280. - `namespace` prefixes and groups the exposed tool name.
  281. - `codemode` defaults to `true` and makes the tool available through the
  282. `execute` CodeMode tool. Set `codemode: false` to expose it directly to the
  283. provider.
  284. The executor receives a second context argument containing `id`, `sessionID`,
  285. `agent`, `messageID`, and `progress`. A tool with `output`
  286. must return `output`; Effect and Standard Schema codecs validate it, while raw
  287. JSON Schema definitions enforce JSON compatibility only. A tool
  288. without `output` returns model-visible `content` instead.
  289. ### Add a command
  290. ```js title=".opencode/plugins/review-command.js"
  291. import { Plugin } from "@opencode-ai/plugin"
  292. export default Plugin.define({
  293. id: "acme.review-command",
  294. setup: async (ctx) => {
  295. await ctx.command.transform((commands) => {
  296. commands.update("review", (command) => {
  297. command.description = "Review the current changes"
  298. command.template = "Review the current changes for correctness and missing tests."
  299. })
  300. })
  301. },
  302. })
  303. ```
  304. ### Set the default model
  305. ```js title=".opencode/plugins/default-model.js"
  306. import { Plugin } from "@opencode-ai/plugin"
  307. export default Plugin.define({
  308. id: "acme.default-model",
  309. setup: async (ctx) => {
  310. await ctx.catalog.transform((catalog) => {
  311. catalog.model.default.set("anthropic", "claude-sonnet-4-5")
  312. })
  313. },
  314. })
  315. ```
  316. ## Publish a package
  317. A package plugin uses the same default export as a local plugin. A minimal
  318. manifest is:
  319. ```json title="package.json"
  320. {
  321. "name": "opencode-acme-plugin",
  322. "version": "1.0.0",
  323. "type": "module",
  324. "exports": "./src/index.ts",
  325. "dependencies": {
  326. "@opencode-ai/plugin": "beta"
  327. }
  328. }
  329. ```
  330. Use versions compatible with the OpenCode release you target and test the
  331. installed package, not only a workspace-linked copy. Because the plugin API is
  332. beta, publish compatible plugin updates when V2 entrypoints or contracts
  333. change.
  334. ## Verify loading
  335. List active plugin IDs through the V2 API:
  336. ```sh
  337. opencode2 api get /api/plugin
  338. ```
  339. If a plugin is absent, check the server log described in
  340. [Troubleshooting](/troubleshooting#read-logs). Invalid modules and setup failures are
  341. logged; one failing package does not prevent unrelated valid packages from
  342. being resolved.
  343. ## Effect
  344. OpenCode provides a first-class Effect API for plugins through the
  345. `@opencode-ai/plugin/effect` entrypoint. Install `effect` alongside the
  346. plugin package and export an `effect` function instead of `setup`:
  347. ```sh
  348. bun add @opencode-ai/plugin@beta effect
  349. ```
  350. ```ts title=".opencode/plugins/reviewer-effect.ts"
  351. import { Plugin } from "@opencode-ai/plugin/effect"
  352. import { Effect } from "effect"
  353. export default Plugin.define({
  354. id: "acme.reviewer-effect",
  355. effect: (ctx) =>
  356. Effect.gen(function* () {
  357. yield* ctx.agent.transform((agents) => {
  358. agents.update("reviewer", (agent) => {
  359. agent.description = "Reviews code for regressions"
  360. agent.mode = "subagent"
  361. })
  362. })
  363. }),
  364. })
  365. ```
  366. Context operations return Effects. The plugin effect is scoped, so finalizers,
  367. fibers, and registrations are released when the plugin reloads or unloads.
  368. OpenCode does not expose its private Core services to the plugin; use the
  369. capabilities on `ctx`.
  370. Typed tools can use `Schema` from `effect`. Effect and Promise plugins use the
  371. same `tools.add(name, tool, options?)` registration shape. Effect executors
  372. return an Effect and may fail with the typed tool failure channel.