OpenCode plugin that connects to Cursor's API, giving you access to Cursor models inside OpenCode with full tool-calling support.
npx cursor-oauth-opencode setup --global
opencode auth login --provider cursorFor project-local setup:
npx cursor-oauth-opencode setup --project
opencode auth login --provider cursorThe setup command is idempotent. It adds the npm plugin entry and fallback
model registries for both provider.cursor and provider.cursor-code in
opencode.json, preserving existing user model overrides. Login stays on the
primary provider only (opencode auth login --provider cursor).
The plugin registers two OpenCode providers that share one OAuth login, proxy, and model catalog. Mode is selected in the model picker:
| Provider | Picker path | Behavior |
|---|---|---|
| Cursor | cursor/<model> |
Native tool passthrough (code mode OFF) |
| Cursor Code | cursor-code/<model> |
Single code sandbox tool (code mode ON) |
Breaking default: cursor/* is native-tools / code mode OFF. To keep the
previous sandbox-collapse behavior, pick cursor-code/<model>. There is no
auto-rewrite of a previously selected cursor/* model.
Fleet-wide kill switch (forces OFF even for cursor-code/*): set
CURSOR_OPENCODE_CODE_MODE=0 or provider.cursor.plugin.codeMode.enabled: false.
That knob is read only under provider.cursor.plugin — a block under
provider.cursor-code.plugin has no effect.
Reasoning-tier Cursor models are listed once in the picker; effort is chosen in OpenCode's Select variant menu (not as separate model slugs):
| Picker model | Variants | Notes |
|---|---|---|
grok-4.5-fast |
low / medium / high |
Cursor slug suffixes are shifted (high → …-xhigh); non-fast grok-4.5 is omitted |
claude-4.6-opus, gpt-5.4, glm-5.2, … |
only levels Cursor exposes | e.g. opus → high, gpt-5.4 → medium |
composer-2.5 / composer-2.5-fast |
default only |
Non-effort axis; stay as separate models |
Legacy full slugs (grok-4.5-fast-high, …) still resolve as aliases. After
upgrade, re-run setup so opencode.json seeds the collapsed base ids.
If you do not want to use the setup command, add this to
~/.config/opencode/opencode.json:
The fallback API URLs are placeholders so OpenCode can list the providers
before login. After auth, the plugin starts a local proxy and replaces Cursor
models with live Cursor model discovery. cursor-code must keep the /v1/code
path suffix (merge-safety if a request falls back to provider.api).
opencode auth login --provider cursorThis opens Cursor OAuth in the browser. Tokens are stored in
~/.local/share/opencode/auth.json and refreshed automatically. Both
providers use that single cursor auth entry.
Start OpenCode and select a model under Cursor (native tools) or
Cursor Code (sandbox code tool). The plugin starts a local
OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API.
- OAuth — browser-based login to Cursor via PKCE (
cursorprovider only). - Model discovery — queries Cursor's gRPC API for all available models.
- Local proxy — translates chat completions into Cursor's protobuf/HTTP/2
Connect protocol. Path selects mode:
/v1/*(native) vs/v1/code/*(sandbox). - Code mode (
cursor-code) — advertises only the MCPcodetool to the Cursor model (allowed-tools hard-forced tomcpToolCall); the model writes one async JavaScript program that the proxy runs in a Node child sandbox, bridging eachawait tools.X()wave to OpenCode's tools while the Cursor bridge stays parked. Native read/list/grep/mutations are rejected with coaching; a leaked native shell or AskQuestion uses the controlled client bridge and is counted by the probe.cursor/*passes client tools through as MCP tools (subject to native policy below). - Request continuity — every plugin window and OpenCode session carries an immutable proxy scope. Parked tool calls route by session plus pending call ids. A valid Cursor checkpoint is authoritative when workspace identity and client-history prefix match; missing, corrupt, incomplete, cross-workspace, or history-diverged checkpoints append one model-visible recovery marker and rebuild from the client ledger/history instead of borrowing another workspace's state.
HTTP/2 transport runs through a Node child process (h2-daemon.mjs / legacy
h2-bridge.mjs) because Bun's node:http2 support is not reliable against
Cursor's API.
These knobs live under provider.cursor.plugin (env overrides last). They
describe how this plugin talks to Cursor's private Agent Run protocol — that
surface is not a stable public API and can change without notice.
| Knob | Default | Env | Behavior |
|---|---|---|---|
| (client version) | manifest pin cli-2026.01.09-231024f |
CURSOR_OPENCODE_CLIENT_VERSION |
Advertised x-cursor-client-version is the checked-in protocol manifest pin, not "newest installed cursor-agent". The env value is an explicit development override only. |
requestContext.inline |
true |
CURSOR_OPENCODE_INLINE_REQUEST_CONTEXT |
When on, the initial Run carries MCP tools plus inline RequestContext (same catalog used for refresh replies). Set false / 0 to roll back to refresh-only context. |
nativePolicy |
legacy |
CURSOR_OPENCODE_NATIVE_POLICY |
legacy: model-visible MCP declarations match the full client catalog. dedupe-canary: omit MCP declarations for native-eligible read/list/grep roles while keeping the full execution catalog for bridges. Code mode always declares only code. |
| (Run retries) | 3 |
CURSOR_OPENCODE_RUN_RETRIES |
Maximum automatic retries for zero-byte transport failures and retryable Connect errors. Before output, the original action is replayed; after output, a safe latest checkpoint is resumed with ResumeAction. Set 0 to disable. |
| (retry backoff) | 500 ms |
CURSOR_OPENCODE_RUN_RETRY_BACKOFF_MS |
Delay before retrying resource_exhausted / unavailable Connect failures. |
Declaration vs execution catalogs: the proxy keeps the complete client tool
list internally for shell/question/MCP bridges even when dedupe-canary hides
duplicate read/list/grep MCP definitions from the model.
Circuit breaker: if dedupe-canary hits an unhandled or duplicate/closed
exec, or a transport close before turnEnded, new Runs in that proxy context
fall back to legacy declarations. In-flight execs still finish with their
matching native result/error type.
Exec lifecycle / cancel: each controlled exec gets a heartbeat while
active, a typed result or throw, and exactly one streamClose. Client abort
(Esc) sends protocol CancelAction and tears down the owned bridge, execs,
timers, and code sandbox.
On every outbound Cursor Run the proxy can:
- Caveman-compress tool-description prose and the OpenCode system prompt
(
plugin.caveman.level/CURSOR_OPENCODE_CAVEMAN, defaultfull; system inherits viaplugin.caveman.system/CURSOR_OPENCODE_CAVEMAN_SYSTEM). - Append a verbatim reasoning guard for runaway-reasoning models
(
claude-fable-5,claude-opus-4-8;CURSOR_OPENCODE_REASONING_GUARD=0). - Inject a coalesced harness appendix (interstitial text, overthink
<system-reminder>, throttled continuity, single-shot[reasoning tail]/[thinking exhaustion]notes) for those same models.
Savings counters live under CURSOR_OPENCODE_SAVINGS=1 (plugin log file).
OpenCode --> /v1/chat/completions (cursor = code OFF)
--> /v1/code/chat/completions (cursor-code = code ON)
|
Bun.serve (proxy)
|
Node child (h2-daemon / h2-bridge)
|
HTTP/2 Connect stream
|
api2.cursor.sh gRPC
/agent.v1.AgentService/Run
1. Proxy advertises ONE code tool to the Cursor model (its description embeds
OpenCode's real tool catalog)
2. Model emits code({script}); proxy runs the script in a Node child sandbox
3. Each await tools.X() wave -> proxy emits OpenAI tool_calls SSE, parks BOTH
the Cursor bridge and the sandbox
4. OpenCode executes the tools, sends results in the follow-up request
5. Proxy feeds results to the parked sandbox; the next wave repeats step 3
6. Script returns -> its value becomes the code tool result; the Cursor bridge
resumes and streams the model's final answer
With cursor/* (or the kill switch forcing OFF) the proxy passes each client
tool through directly: the model calls a tool via mcpArgs, the proxy emits
OpenAI tool_calls, pauses the H2 stream, and resumes with mcpResult on the
follow-up request.
bun install
bun run build
bun run test
bun run test:serial # serial diagnostics
bun run test:smoke # parallel smoke groups onlyThe default test gate runs protocol snapshot/header/surface checks in parallel,
then six isolated Bun smoke groups with five-way parallelism through Vitest.
Tests sharing module state remain serial within a group. Run one smoke group with
bun run test:group <name>; names: core, index-context, code-mode,
streaming, transport, utilities.
Plugin diagnostics never print to the terminal (they would paint over the
OpenCode TUI). They land in ~/.cache/cursor-oauth-opencode/plugin.log.
Override the path with CURSOR_OPENCODE_LOG_FILE; set
CURSOR_OPENCODE_LOG_STDERR=1 to also mirror lines to stderr when debugging
outside the TUI.
{ "$schema": "https://opencode.ai/config.json", "plugin": [ "cursor-oauth-opencode@latest" ], "provider": { "cursor": { "name": "Cursor", "npm": "@ai-sdk/openai-compatible", "api": "http://127.0.0.1:65535/v1", "models": { "composer-2.5-fast": { "name": "Composer 2.5 Fast", "reasoning": true, "temperature": true, "attachment": false, "tool_call": true, "limit": { "context": 200000, "output": 64000 } } } }, "cursor-code": { "name": "Cursor Code", "npm": "@ai-sdk/openai-compatible", "api": "http://127.0.0.1:65535/v1/code", "models": { "composer-2.5-fast": { "name": "Composer 2.5 Fast", "reasoning": true, "temperature": true, "attachment": false, "tool_call": true, "limit": { "context": 200000, "output": 64000 } } } } } }