Dedicated entrypoint: cursor-opencode-provider/plugin/opencode2. Do not also load the classic plugin entry or plugin/v2 under OpenCode 2.0 — they target OpenCode 1.x APIs.
For OpenCode 1.x, see opencode-1.md. Shared catalog behavior (variants, 1M, Fast, images) lives in the root README.
If you previously ran this plugin against OpenCode 2.0 next (ctx.catalog) or against a build that wrote Cursor models into opencode.json, start at Safe transition.
This guide follows the development checkout. Check Unreleased for fixes that require a rebuilt local install until the next release.
Discovered Cursor models are published in memory through ctx.provider.transform → editor.add({ info, models }) → ctx.provider.reload(). The plugin does not write a model list into opencode.json.
Cache seed (and /connect afterwards) refill the inventory. An empty first transform is a no-op, so a failed refresh keeps the last successful list.
You do not need a hand-written providers entry, and there is no enabled flag.
Synced models set time.released to 0 (Cursor AvailableModels does not expose release dates). OpenCode 2.0's picker sorts by released descending, so filter by provider Cursor or search a model id if the global list looks empty.
{
"plugin": ["cursor-opencode-provider/plugin/opencode2"]
}Pin a version if you want: "cursor-opencode-provider@0.8.2/plugin/opencode2".
OpenCode 2.0 installs the published package into its host cache and loads the AI SDK entry from there (aisdk:cursor-opencode-provider). No extra env vars are required.
opencode2 defaults to the same ~/.config/opencode as OpenCode 1.x unless you set OPENCODE_CONFIG_DIR. Prefer a dedicated directory so 1.x plugin / provider entries and 2.0 plugins/ do not share one file:
export OPENCODE_CONFIG_DIR=~/.config/opencode2If npm subpath resolution is unreliable, install a plugin directory under $OPENCODE_CONFIG_DIR/plugins/ (OpenCode 2.0 rejects a bare .js path in config — it must be a directory with package.json):
mkdir -p "$OPENCODE_CONFIG_DIR/plugins/cursor"
cat > "$OPENCODE_CONFIG_DIR/plugins/cursor/package.json" <<'EOF'
{ "name": "cursor-local", "type": "module", "main": "./index.js" }
EOF
cat > "$OPENCODE_CONFIG_DIR/plugins/cursor/index.js" <<EOF
export { default } from "/absolute/path/to/cursor-opencode-provider/dist/plugin-opencode2.js"
EOFInside opencode2, run /connect, choose Cursor, then browser login or an API key. CURSOR_API_KEY is also picked up automatically.
After auth, Cursor models appear in the picker from the in-memory inventory. The <host-cache>/cursor-models.json cache is still used as the discovery source; it is not copied into config.
The 500k max tier appears as grok-4.7-500k (and grok-4.7-500k-fast),
with its own context limit and variant list. These entries still send
grok-4.7 to Cursor and retain the tier's declared default effort;
see shared model selection.
Saved 500k variants on the original base/Fast entries remain available after
upgrading. Select the dedicated 500k entry for its full host context budget.
The plugin supplies its own Cursor SDK even when the host's generic package loader already created one. Each new Run resolves the active integration credential and applies on-demand renewal; a token captured by the generic loader or its options cannot bypass that resolver.
cd /absolute/path/to/cursor-opencode-provider
bun install && bun run buildLoading only the local plugin file is not enough for end-to-end local testing: OpenCode 2.0 still resolves the AI SDK package through npm install into <host-cache>/packages/… unless you override it. Point CURSOR_OPENCODE2_DEV_ENTRY at the built provider entry (dist/index.js, which exports createCursor) so the host imports that file directly.
export OPENCODE_CONFIG_DIR=~/.config/opencode2
export CURSOR_OPENCODE2_DEV_ENTRY=/absolute/path/to/cursor-opencode-provider/dist/index.jsExample config that loads the local package (directory path — not a bare .js file):
{
"plugin": [
"/absolute/path/to/cursor-opencode-provider"
]
}Prefer the $OPENCODE_CONFIG_DIR/plugins/<name>/ package directory shown above. For the AI SDK entry, still set CURSOR_OPENCODE2_DEV_ENTRY to dist/index.js.
-
Export the env var before starting the daemon.
opencode2 serve/service startinherit env at start time. After changingCURSOR_OPENCODE2_DEV_ENTRY,OPENCODE_CONFIG_DIR, or rebuildingdist/, restart the service:opencode2 service stop export OPENCODE_CONFIG_DIR=~/.config/opencode2 export CURSOR_OPENCODE2_DEV_ENTRY=/absolute/path/to/cursor-opencode-provider/dist/index.js opencode2 service set env OPENCODE_CONFIG_DIR "$OPENCODE_CONFIG_DIR" opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY "$CURSOR_OPENCODE2_DEV_ENTRY" opencode2 service start
-
Unset
CURSOR_OPENCODE2_DEV_ENTRYin production so the daemon uses the publishedaisdk:cursor-opencode-providerpackage again. -
OpenCode does not install dependencies for local plugin files. Keep the clone's
bun installintact and load the plugin from inside the clone (a copied-out file will not resolveprotobufjs). -
Rebuild after every change (
bun run build). The daemon readsdist/, notsrc/. -
Plugin reload does not refresh the running AI SDK. The daemon caches SDKs and language models separately from plugin hooks. A new plugin
copy=#…header can therefore appear while turns still use earlier code. Fully restart the daemon after rebuilding; for cost-reporting changes, confirm freshbilling: step=records on tool and final finishes.
OpenCode 2.0's long-lived daemon often starts from $HOME (or another spawn cwd), so process.cwd() is not the active project. The provider resolves the session workspace in this order:
- Request header
x-opencode-directory(URI-encoded absolute path; per-request). The plugin adds it onsession.hook("model.request")for thecursorprovider fromctx.session.get()→info.location.directory, falling back to the last known session directory, thenctx.location.directory. It rides the AI SDK call options only and is never sent to Cursor, so it does not depend on in-memory state shared with the module copy that runs the model. - Session mark from session hooks →
ctx.session.get()(info.location.directory) — fallback within the same module copy only - Static
createSdk({ workspaceRoot })/ process cwd as last resort
The plugin also forces OpenCode 2's path / shell tool dialect when advertised schemas are opaque, so bridged file tools do not fall back to OpenCode 1.x filePath / bash under a multi-project daemon.
OpenCode 2.0 routes MCP servers through Code Mode unless their config sets codemode: false (the host default is true). Code Mode tools do not reach the model individually: the provider only sees a single execute tool, so Cursor cannot call an MCP tool such as github_create_pull_request by name.
The plugin leaves that server config alone. codemode on the server also decides whether OpenCode asks a remote MCP server for its raw tools. Catalog placement is separate: ctx.tool.transform sets options.codemode: false on tools whose namespace belongs to an MCP server that did not set "codemode": true. Those tools join the direct catalog. Explicit "codemode": true stays inside execute, as do OpenCode's own Code Mode tools.
The tool registry is shared by every provider in the process. A server that should stay in Code Mode for other models needs "codemode": true in its MCP config.
Cursor layer (OpenCode 1.x and 2.0): joining the AI SDK / direct catalog is what lets this provider advertise the tool in RequestContext. Cursor still keeps those tools off its native top-level function list — models reach them through GetDynamicTools / CallDynamicTool (and the provider's mcp_args exec path). When the host advertises skill and/or tools of MCP servers configured in opencode.json, shared interaction guidance names those routes and mirrors OpenCode's load-when-matches / do-not-reinvoke rules. Mid-Conversation skill reminders fire only when the available-skills catalog changes after baseline admission (same contract as OpenCode SkillGuidance / SkillInstructions) — not on every user turn that happens to mention a skill id (issue #29). Classic OpenCode 1.x already puts MCP tools in the AI SDK catalog; only OpenCode 2 needs the direct-catalog transform above.
No extra configuration is needed. Declare MCP servers in opencode.json as usual:
{
"mcp": {
"servers": {
"github": { "type": "local", "command": ["github-mcp-server", "stdio"] },
"docs": { "type": "remote", "url": "https://example.com/mcp" },
"executor": { "type": "local", "command": ["my-executor"], "codemode": true }
}
}
}github and docs leave codemode unset, so their tools join the direct catalog and this provider can advertise them to Cursor (for example github_create_pull_request via GetDynamicTools / CallDynamicTool). executor sets "codemode": true and stays inside execute. The plugin does not rewrite these entries.
Server names are normalized into tool namespaces (my.docs becomes my_docs). If names collide, an explicit "codemode": true prevents this plugin from moving that namespace. The opencode namespace is always left alone to preserve OpenCode's own tools; choose another MCP server name to use automatic direct placement.
OpenCode's <mcp_instructions> still say to use execute for a server that left codemode unset, because that sentence reads the server config and not the tool option. The provider guidance tells Cursor to ignore that sentence for tools that are on the direct list. Discovery reloads replay the tool transform, so tools that connect after startup join the same catalog.
A prompt sent before those servers finish connecting will not see their tools. OpenCode connects MCP servers asynchronously and does not block startup on a slow server. The next turn in the same session includes the tools that have connected. That first-turn gap is expected host behavior, not a missing catalog placement.
| Classic plugin (OpenCode 1.x) | OpenCode 2.0 plugin |
|---|---|
config hook registers provider + models |
ctx.provider.transform + editor.add + reload() |
auth hook (OAuth + API key) |
ctx.integration.transform + /connect |
tool hook (custom_websearch, cursor_image_save) |
ctx.tool.transform registers handle-only cursor_image_save (codemode: false, permission: "edit", output schema) so generated images can be committed. todowrite/todoread only when CURSOR_OPENCODE2_TODOS=1 (or true) and the host catalog omits them. Off by default. OpenCode 2's native websearch tool owns permission checks and uses the provider registered through ctx.websearch.transform. The public 2.0 ToolContext has no permission-request method, so this entrypoint does not advertise a direct web-search fallback; image save uses catalog edit gating plus containment when ask is absent. |
event hook (session activity) |
ctx.event.subscribe |
tool.execute.before / .after |
ctx.tool.hook(...) |
Post-approval shell.env injects the timeout wrapper for bash/zsh |
ctx.shell.hook("create.before") installs a same-basename executable adapter. The command stays unchanged for the host's later permission scan; the payload uses the actual selected shell. |
chat.params flags compaction turns and names the current agent |
session.hook("context") / "compaction" sets options.opencodeCompaction, options.opencodeHostAgent, and the session marker |
plan_enter / plan_exit |
OpenCode 2's primary plan / build agents are selected through session.switchAgent. Cursor mode entry is applied after its Run reaches a safe terminal boundary. CreatePlan records the plan in the Plan directory, shows it, and asks through question whether to switch to build and start implementing; Yes selects build and continues the turn. |
Host todowrite builtin |
OpenCode 2 dropped host todo tools. Plugin-owned todowrite/todoread stay off unless CURSOR_OPENCODE2_TODOS=1 (or true). When enabled they register as direct catalog tools (codemode: false + output schema, in-memory per session) unless editor.get/list already owns those names. With the flag unset, no provider todo tools are registered. Cursor TodoWrite/TodoRead mirror into them only when they are advertised. OpenCode 1.x still uses the host builtin and is not gated. |
Host task tool |
Native Cursor Task remaps onto advertised task, or onto OpenCode 2 subagent (agent / sessionID / background) when task is absent |
| — | credential.switched clears the token cache and reloads models |
| — | editor.add({ sourceConnection }) binds inventory to the active Cursor connection |
| — | ctx.websearch.transform publishes Exa results as {url,title,content,time} for OpenCode 2's native permission-gated websearch tool |
Package root / plugin |
OpenCode 2 Host.resolve loads exports["./server"] → this entry ({ id, setup }). The same module dual-exports server: CursorPlugin so OpenCode 1.18 still gets the classic plugin. |
Shell output keeps path-like text, whitespace and successful timeout-shaped text unchanged. Internal wrapper markers are removed without trimming command output, and failed shell streams retain partial output alongside their error. A host shell failure reaches Cursor once as stderr in streamed and unary results; distinct command output remains on stdout.
The shell integration follows each host's execution order. OpenCode 1.18.31
checks permissions before shell.env.
OpenCode 2.0.27 runs create.before before its permission callback and then spawns the selected executable,
so the adapter keeps the original command and shell basename intact for
the native scanner.
OpenCode 2's AI SDK conversion lowers tool errors to ordinary text.
The session hooks preserve the native result status in each tool part's provider
metadata before conversion, so reads, writes, deletes and other tools remain
failures in continuations and replayed history. The tool after hook retains typed
permission causes to distinguish configured denials from user rejection;
successful output that resembles an error is left successful.
A decline without feedback bypasses the tool after hook.
The permission and terminal-failure events release unstarted shell reservations
immediately; running jobs retain their records.
Generated-image saving uses the same handle-only cursor_image_save tool on classic and OpenCode 2.0. Classic/OCP surfaces raise external_directory then edit before the write. Stock OpenCode 2.0 has no public ask, so the 2.0 entrypoint still advertises the tool (permission: "edit" so agents that deny edits do not get it) and commits after containment; the id is opaque, so the model cannot write arbitrary files.
Cursor Task attachments reach the native subagent prompt as exact, quoted
file paths, retaining Cursor's description input. The child opens each file
through its own permission-checked tools; attachment bytes are not read into
the parent. Choose an image-capable child model for image inspection. See
shared subagent routing.
OpenCode 2 selects patch (input { patchText }) for eligible GPT models and
removes edit / write. The provider accepts direct dynamic patch calls and
converts Cursor-native file execs from the current permitted catalog. It refuses
patch operations that would lose CRLF bytes before changing a file. Native
edit / write remain unchanged when permitted; see shared patch behavior.
A plain-text steer without files or skills can reach an actively pumping Cursor Run before OpenCode reaches its next step boundary, including during AwaitShell. Messages that cannot be admitted early retain the normal host delivery path. Answered copies are deduplicated across restart/retry, while later identical requests, unmatched messages and attachments remain intact. If a Run fails after an unanswered steer was promoted or checkpointed and recovery cannot preserve it, the next attempt rebuilds from host history to retain the message. Stop notifies the Cursor Run and saves recoverable state; the next turn preserves the checkpoint when safe and otherwise rebuilds from host history. See 2.0 incident lessons and acceptance checks.
OpenCode 2 owns its full system prompt and its vendor-maintained Plan agent. The provider forwards the host prompt verbatim as Cursor's always-apply system-instructions rule (Cursor does not follow a client-seeded system message); it does not copy or replace the 2.0 prompt templates. Provider-added guidance is limited to protocol facts the host cannot know, such as Cursor interaction bridging and the advertised direct tool catalog.
MCP tools whose server did not set "codemode": true are on the direct catalog and are called by name. execute remains Code Mode for tools that are still absent from that list, including an explicit "codemode": true server and OpenCode's own Code Mode tools. Cursor calls execute with { code } and uses the exact paths and signatures from the host Code Mode catalog (or its search function). OpenCode still applies its own tool availability and permission checks when execute runs.
When Cursor raises SwitchMode for plan or spec, the plugin selects OpenCode 2's plan primary agent after the current Cursor Run has safely ended. Approved non-plan targets select build. While the session runs the plan agent and no native plan_exit tool exists, a Cursor SwitchMode out of plan mode is declined with a pointer to CreatePlan, whose question prompt is the approval to start implementing (without question, the user switches agents). A user switching agents in the OpenCode UI follows the same state path because session.hook("context") carries the active agent into the provider.
An OpenCode agent or system-prompt change keeps the sticky Cursor conversation, as Cursor CLI keeps its conversation across mode flips. The conversation's first system context stays frozen as the system-instructions rule, so the prompt cache survives; the new agent and the changed host instructions arrive as a Mid-Conversation System Message on the next user turn, which the model follows. The host agent and prompt hash are persisted with the checkpoint for diagnostics only. Ephemeral title/generation calls do not alter that identity.
Three plugin shapes have existed for OpenCode 2.0. Only the last one is current.
| Era | How models showed up | Status |
|---|---|---|
OpenCode 2.0 next (@opencode-ai/plugin@next, ctx.catalog) |
In-memory ctx.catalog.transform |
Removed. OpenCode 2.0 has no ctx.catalog. |
Config dump (55126a8 and follow-ups) |
Surgical JSONC upsert into $OPENCODE_CONFIG_DIR/opencode.json(c) → providers.cursor (often a large models map) |
Deprecated. The plugin no longer writes config. A leftover providers.cursor is a second writer and can fight the in-memory inventory. |
| Current | In-memory ctx.provider.transform + editor.add + reload() |
Use this. Nothing is written into opencode.json. |
55126a8 (fix(opencode2): support stable 2.0.x without ctx.catalog) is the commit that introduced the opencode.json dump: stable OpenCode 2.0 had dropped ctx.catalog, so the plugin copied the discovered list into providers.cursor. That file could grow to thousands of lines. The current plugin replaces that workaround with the host's in-memory provider editor.
-
Stop the daemon so it cannot rewrite config while you edit it:
opencode2 service stop
-
Use a dedicated config dir (recommended even if you already do):
export OPENCODE_CONFIG_DIR=~/.config/opencode2
Mixing OpenCode 1.x
plugin/provider.cursorwith OpenCode 2.0plugins/in~/.config/opencodeis the usual source of “models vanished after upgrade” reports. -
Back up config before deleting the dump:
cp "$OPENCODE_CONFIG_DIR/opencode.json" "$OPENCODE_CONFIG_DIR/opencode.json.bak"
-
Install the current plugin (published
plugin/opencode2, or a rebuilt local clone — see above). Do not keep a next-era or dump-eradist/loaded via$OPENCODE_CONFIG_DIR/plugins/cursor.
Open $OPENCODE_CONFIG_DIR/opencode.json (or opencode.jsonc).
-
Delete the entire
"cursor"object under"providers". -
If
"providers"is then empty, delete"providers"too. -
Keep MCP, permissions,
plugin/plugins/entries, and anything else you added. -
Do not leave a stub such as:
"providers": { "cursor": { "name": "Cursor", "package": "aisdk:file:///…/dist/index.js", "integrationID": "cursor" } }
OpenCode 2.0 still applies that block through its built-in config-provider plugin. Name, package, and integration belong to the in-memory inventory now.
You can leave <host-cache>/cursor-models.json alone. That cache is discovery input, not the deprecated config dump.
Old opencode.json.bak-* copies from the dump era are safe to delete after the picker works.
Config should load one of:
"plugin": ["cursor-opencode-provider/plugin/opencode2"](npm)- a
$OPENCODE_CONFIG_DIR/plugins/<name>/directory that re-exportsdist/plugin-opencode2.js(local)
Remove, if present:
"plugin": ["cursor-opencode-provider"](classic 1.x)"plugins": ["cursor-opencode-provider/plugin/v2"]- any
file://…/dist/plugin.jsorplugin-v2.jspath under this config dir @opencode-ai/plugin-next/ “next” plugin pins in this package (the current 2.0 entrypoint duck-types the host; it does not depend on@opencode/plugin)
opencode2 service set env OPENCODE_CONFIG_DIR "$OPENCODE_CONFIG_DIR"
# local clone only:
# opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY /absolute/path/to/cursor-opencode-provider/dist/index.js
opencode2 service startThen /connect → Cursor if credentials are missing. Filter the picker by provider Cursor. Models should appear without a providers.cursor block.
| Problem | What to try |
|---|---|
| No Cursor models in the picker | /connect → Cursor (or shared auth.json). Dedicated OPENCODE_CONFIG_DIR. Plugin is a directory re-exporting plugin/opencode2, not a bare .js. Filter by provider Cursor (time.released is 0). Remove leftover providers.cursor (see Safe transition). |
| Local daemon still runs the published package | Set CURSOR_OPENCODE2_DEV_ENTRY to an absolute …/dist/index.js path before start, persist it with opencode2 service set env, rebuild, restart. Loading only dist/plugin-opencode2.js is not enough. |
| The model cannot find an MCP tool and searches files for it instead | The server set "codemode": true, so its tools stay inside execute. Remove that field to put them on the direct catalog; see MCP tools. |
| A prompt immediately after startup does not see MCP tools | Expected. OpenCode connects MCP servers asynchronously and does not block startup on a slow server. The next turn in the same session includes tools that have connected. Do not treat this as a catalog bug or delay startup to wait for MCP. |
| Picker / auth broke after an upgrade | You are likely still on the next-era ctx.catalog build or the dump-era plugin. Follow Safe transition and restart. |
opencode.json ballooned to thousands of lines |
That was the dump-era providers.cursor.models map. Remove providers.cursor as above. The current plugin will not recreate it. |
401 / "sign in to Cursor again" |
A browser login is renewed through Cursor's /oauth/token when a request needs a token and less than 1272 h of the 60-day session remain. The credential stores the JWT expiry as expires; the 2.0 host refreshes it within five minutes of that and persists the renewal the plugin already holds in memory. A daemon that serves no request for the whole 60 days lets the session expire; reconnect (/connect → Cursor) then. API keys (/connect key or CURSOR_API_KEY) are re-exchanged within five minutes of the exchanged JWT's expiry. |