cursor-oauth-opencode
v0.22.1
Published
OpenCode plugin that connects Cursor's API to OpenCode via OAuth, model discovery, and a local OpenAI-compatible proxy.
Maintainers
Readme
cursor-oauth-opencode
OpenCode plugin that connects to Cursor's API, giving you access to Cursor models inside OpenCode with full tool-calling support.
Install
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 package supports the OpenCode v1 hooks loader and the unreleased OpenCode
v2 plugin API from one install. Its ./server entry is dual-shaped: v1 invokes
server, while v2 schema-decodes and invokes setup. OpenCode v2 normalizes
the legacy plugin / provider config keys used by setup, so the same config
works on both runtimes. v2 support remains provisional until upstream ships a
versioned release.
Providers
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> | OpenCode-backed native tools plus residual MCP tools (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.
OpenCode skill routing (lowercase skill tool) is on by default and constrained
to the system <available_skills> catalog. Disable with
CURSOR_OPENCODE_SKILLS=0 or provider.cursor.plugin.skills.enabled: false.
Missing/empty/malformed catalogs fail closed (skill tool stripped), not free-string.
Normal cursor/* Runs derive one capability plan from the current OpenCode tool
catalog. Exact, schema-compatible built-ins use Cursor's fixed native protocol;
tools without a safe native equivalent remain ordinary MCP declarations:
| OpenCode tool | Cursor-native carriers |
|---------------|------------------------|
| read | Read, Ls, PI Read, PI Ls |
| grep | Grep, PI Grep |
| bash | Shell, streaming shell, mini-SWE bash, PI Bash |
| glob | Glob (grepArgs files-with-matches carrier) |
| question | AskQuestion |
| task | Task/subagent with OpenCode's dynamic agent catalog |
| lsp | ReadLints diagnostics capture facade (production gate closed); general LSP stays MCP-visible |
| apply_patch | Delete only; full apply_patch remains MCP-visible |
Promotion requires the exact lowercase built-in name, its trusted OpenCode schema, and one unambiguous owner. Aliases, namespaced tools, incompatible schemas, and custom/plugin tools stay MCP. A Run has zero MCP declarations only when it has no residual tools. OpenCode still executes every mapped operation and owns its permission checks.
Native Glob uses Cursor's live globToolCall, which executes through a
glob-shaped grepArgs / grepResult wire while OpenCode still owns the exact
glob operation. The protocol's fetchToolCall has a complete
fetchArgs / fetchResult client Exec pair, but grok-4.5-fast low does not list
or emit it even when explicitly allowed, so webfetch remains MCP-visible. The
separate approval-only webFetchToolCall also stays disabled because it cannot
carry OpenCode's result. Native Task is enabled only when the exact task description
contains OpenCode's parseable, sorted dynamic agent catalog; otherwise task
stays MCP-declared. The capture-only ReadLints facade requires a canonical lsp
schema with a diagnostics operation and maps zero-based ranges, LSP severity
1-4, file/permission/error variants, and exact Exec/tool identities to Cursor's
typed diagnosticsResult. Production promotion remains disabled: OpenCode
1.18.4 exposes no public diagnostics operation, and the pinned live Grok low
probe completed readLintsToolCall without sending the client
diagnosticsArgs/diagnosticsResult handoff. PI Edit stays disabled in
transparent mode: two low-effort grok-4.5-fast attempts produced MCP or no
ToolCall when it was allowed, and hiding raw apply_patch would remove add,
move, and multi-file operations that PI Edit cannot represent. Standard
editToolCall stays disabled because it has no client Exec/result lane. Default
write remains MCP because live capture produced only unpaired standard
writeArgs/writeResult traffic toward Cursor agent-tools, with no complete
standard or PI ToolCall identity and target sequence.
Cursor-native WebSearch is enabled only when OpenCode did not advertise its own
lowercase websearch tool. cursor-code/* is unchanged: its protocol allowlist
remains exactly the single MCP code tool.
Reasoning variants
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.6 / grok-4.6-fast | low / medium / high / xhigh | Cursor default is high; Fast is a separate picker family (2× price). Wire slugs are cursor-grok-4.6-{effort}[-fast] |
| 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.
Manual config
If you do not want to use the setup command, add this to
~/.config/opencode/opencode.json:
{
"$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
}
}
}
}
}
}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 v2's config normalizer accepts this v1-shaped plugin / provider
document and projects it into plugins / providers. If a v2-native
plugins entry already registers this package, setup recognizes its string or
{ "package": "cursor-oauth-opencode" } form and does not add a duplicate.
Authenticate
opencode auth login --provider cursorThis opens Cursor OAuth in the browser. OpenCode v1 stores tokens in
~/.local/share/opencode/auth.json; v2 stores them as the cursor integration
credential. Both refresh automatically, and both providers share that one
Cursor login.
When refreshing a published plugin install, remove only
~/.cache/opencode/packages/cursor-oauth-opencode@latest. Do not delete
~/.local/share/opencode/auth.json; plugin cache cleanup does not require a new
OAuth login.
Use
Start OpenCode and select a model under Cursor (native tools) or
Cursor Code (local code tool). The plugin starts a local
OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API.
How it works
- 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 ascode({script,files?}); the proxy runs it in the in-proxy Node child sandbox (code-sandbox.mjs) and drives NDJSON client-tool waves (await tools.X(args)/callTool). Native read/list/grep/mutations are rejected with coaching that points back atcode. The streaming tool-call accumulator is inert in this mode so the outercodename is never validated against the inner client catalog.cursor/*uses the request-aware native mapping above and declares only residual tools through MCP. - 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.
Code mode kill switch: provider.cursor.plugin.codeMode.enabled (default
true). Set false or CURSOR_OPENCODE_CODE_MODE=0 to force normal mode even
on /v1/code.
Rollback: disable code mode with provider.cursor.plugin.codeMode.enabled:
false or CURSOR_OPENCODE_CODE_MODE=0 (normal Cursor tools only). Do not
change Cursor headers, native tool allowlists, protobufs, or normal provider
behavior as part of rollback.
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.
Protocol version and tool policy
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.07.20-8cc9c0b | 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 action and checkpoint ResumeAction carry the same immutable RequestContext snapshot (also used for refresh replies). Set false / 0 to omit both action carriers and roll back to refresh-only context. |
| nativePolicy | transparent | CURSOR_OPENCODE_NATIVE_POLICY | transparent: exact compatible built-ins use request-bound native carriers and only residual tools are MCP-declared. dedupe-canary: preserve the earlier read/list/grep dedupe behavior. legacy: keep the complete client catalog MCP-visible as an operator or circuit-breaker rollback. Code mode always declares only code. |
| codeMode.enabled | true | CURSOR_OPENCODE_CODE_MODE | true enables code mode on /v1/code; false / 0 forces normal mode even when path is /v1/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: one immutable plan owns the model-visible residual MCP catalog, complete internal execution catalog, exact native bindings, RequestContext instructions, and allowed-tools header for a physical Run. Refresh, retry, and checkpoint resume reuse that snapshot.
Filesystem ownership: Cursor-native Read and Ls delegate to the
advertised OpenCode read tool; Cursor-native Grep delegates to OpenCode
grep. The proxy translates their results back into Cursor's native result
messages but does not perform the operation. OpenCode therefore owns read,
grep, and external_directory permissions, including for valid absolute
paths outside the workspace.
Shell cwd boundary: existing model-supplied shell directories are
canonicalized and passed to OpenCode, including external directories and
symlink targets; OpenCode owns external_directory approval. Missing paths and
files still fail as typed tool errors before process spawn. Missing absolute
prefixes may use the workspace suffix rescue, and commands are never silently
relocated.
Circuit breaker: if transparent or dedupe-canary sees an impossible
unbound native exec, unhandled case, or duplicate/closed identity, 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.
Tool ownership (normal cursor/*): OpenCode remains the operation and
permission owner. Native dispatch uses the exact binding captured in the Run
plan, never alias search over residual MCP declarations. Unsupported native-only
options return case-correct typed errors rather than being dropped. Fetch, edit,
full patch, resources, skill, todo, plan/general LSP, image/background bridges,
and all custom tools stay MCP unless a separate contract-complete native adapter
is released. A capture-only ReadLints diagnostics adapter is present but its
production gate is closed; navigation, symbols, hover, and call hierarchy never
use it. The native-skill decision, live candidate matrix, and reconsideration
gate are documented in docs/native-skill-carrier.md.
Unsupported cloud subagents, agent stores, SCM/PR/VM/environment
mutation, MCP auth, custom modes, native background, image, and standard native
Write remain typed rejections/errors instead of being advertised optimistically.
Semantic code search remains the plugin tool cursor_codebase_search. It is
local-always with a bounded local result on every call; Cursor cloud is
optional semantic enrichment only if implementation is available after the
concurrent lane and, until then, local remains the sole serving path.
Cursor's semSearchToolCall carries args and
result inline on server interaction updates (ToolCall field 16; args/result
fields 1/2), but the pinned protocol has no matching ExecServerMessage request
or ExecClientMessage result case. It therefore cannot transparently return the
plugin-owned cloud/local result or preserve OpenCode permissions. Code mode
(cursor-code/*) still advertises exactly one MCP code tool and hard-forces
allowed-tools to mcpToolCall only.
Codebase search status: every result starts with these fields in order:
Cursor index adapter session:, Cloud readiness:, Cloud freshness:,
Cloud query outcome:, Serving source:, and Scope:. Readiness is durable
cloud state (disabled, initializing, ready, degraded, or unavailable),
while query outcome and serving source describe only the current call. A valid
cloud query with zero hits therefore remains ready even when local fallback
serves matches. Freshness becomes stale-after-local-change after a successful
bootstrap followed by a relevant watcher change; watcher changes do not claim
continuous cloud upload.
targetDirectories accepts existing workspace-relative directories. Cloud
retrieval remains workspace-wide top-K and decoded paths are post-filtered, so
scoped cloud calls report post-filtered cloud top-K. If every supplied target
is invalid, search does not broaden to the workspace. Local scan, cloud upload,
and returned cloud paths share the same exclusions: ignored build/dependency
directories, every .env*, .npmrc, .pypirc, credential/secret JSON names,
and private-key/certificate extensions (.pem, .key, .p12, .pfx, .crt,
.cer, .der). Local evidence is always available and never depends on cloud;
cloud failure never suppresses local results. When cloud enrichment is
available after the concurrent lane, merged results deduplicate by canonical
relative path and label source truthfully; until then, local remains the sole
serving path.
Cloud bootstrap is adapter-owned and non-blocking for provider load. Concurrent callers join one flight, each RPC resolves the current OAuth token, successful ensure outcomes are memoized, transient failures retry after a bounded cooldown, and manager disposal aborts owned RPCs and waits. The local lane runs concurrently and does not block on cloud.
Search prompt isolation and compaction: codebase search is exposed only as
the explicit cursor_codebase_search tool. The plugin does not add search
guidance or search results to system prompts. After compaction, old Cursor
checkpoints are reused only when compacted client history still has the
checkpoint prefix and every system blob can be safely rebased to the current
system prompt and workspace identity; otherwise state is rebuilt from the
compacted summary and retained tail.
Same-Run transport retry still uses only a safe latest checkpoint with
ResumeAction; the plugin does not invoke Cursor pre-compact or summarization
messages.
Tool bridges live only under provider.cursor.plugin.toolBridges:
| Bridge | Config | Env |
|--------|--------|-----|
| Generate image | toolBridges.generateImage.enabled (+ modelId) | CURSOR_OPENCODE_GENERATE_IMAGE, CURSOR_OPENCODE_GENERATE_IMAGE_MODEL |
| Native Write | toolBridges.nativeWrite.enabled | CURSOR_OPENCODE_NATIVE_WRITE |
| Conversation search (default on) | toolBridges.conversationSearch.enabled | CURSOR_OPENCODE_CONVERSATION_SEARCH |
| Background shell | toolBridges.backgroundShell.enabled | CURSOR_OPENCODE_BACKGROUND_SHELL |
Conversation search is limited to OpenCode sessions in the current workspace; it serves title hits immediately and builds a bounded local message index in the background. It never searches another workspace or Cursor's cloud cache.
Current capture status (release truth): C-IMAGE is
NO_IMAGE_CAPABLE_MODEL / no pass fixture; C-MEDIA has a valid pass fixture
captured from installed OpenCode 1.18.4, but media projection remains
immutable-gated off pending a reviewed release decision; C-WRITE is
NATIVE_WRITE_WIRE_INCOMPLETE: standard writeArgs/writeResult pairs appeared,
but no matching standard or PI ToolCall start/completion identity appeared, and
the emitted writes targeted Cursor agent-tools paths outside the isolated
workspace with nonmatching bytes. Flags do not bypass
those gates — cursor_generate_image and native Write remain unregistered /
runtime-unavailable, and native media projection remains disabled, even if the
flags are set true.
C-SEM-SEARCH was attempted through both a direct AgentService Run and pinned
Cursor Agent 2026.07.20-8cc9c0b with only semSearchToolCall allowed on
grok-4.5-fast low. The account usage gate stopped both attempts before any
ToolCall, so the fixture is unsupported, not a fabricated wire pass. Native
SemSearch remains disabled unless a future pinned descriptor and live capture
prove a client Exec/result handoff that can carry the exact plugin result.
Opt-in MCP background pair (toolBridges.backgroundShell.enabled /
CURSOR_OPENCODE_BACKGROUND_SHELL=1): registers cursor_background_shell
and cursor_await. Requires OpenCode bash permission (and
external_directory when the cwd is outside the worktree). Output is
<project>/terminals/<taskId>.txt. Defaults: 1 MiB output cap, 30 min
runtime (hard max 6 h), 8 jobs/session, await up to 120 s (default 30 s),
completed-job TTL 6 h. Await is same-session only; cancel/dispose tears
down the process group. Registry is process-local — OpenCode/plugin restart
loses in-flight jobs. Native Cursor background/Await stay disabled
regardless.
Prompt shaping (caveman + reasoning containment)
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).
Architecture
OpenCode v1 ./server `server` --> hooks (`auth.loader`, `chat.params`)
OpenCode v2 ./server `setup` --> domains (`integration`, `catalog`, `session`)
|
v
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/RunBoth current loaders prefer one dual ./server module; the root export remains
an older-v1 fallback. The shared runtime lives in src/plugin-core.ts;
src/index.ts is the thin v1 adapter and src/v2-entry.ts is the v2
Promise-domain adapter. v2 pins the
live proxy URL through session.hook("model.request"), so it does not use the
v1 :65535 fetch rewrite. The current v2 plugin tool context exposes neither
the v1 permission prompt nor abort/directory fields, so v2 registers only the
read-only cursor_codebase_search plugin tool; permission-sensitive
background-shell and image bridges remain v1-only until upstream exposes an
equivalent contract. v2 currently uses the host process cwd as workspace
identity because its plugin context does not expose directory/worktree.
Tool call flow (code mode, cursor-code)
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 answerWith 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.
OpenCode todowrite deliberately stays on that MCP path. Cursor's native
updateTodosToolCall and readTodosToolCall carry their arguments and results
inside interaction updates and mutate Cursor's checkpoint-owned todo state;
the current protocol exposes no matching client Exec request/result pair. The
plugin therefore never hides OpenCode todowrite, fabricates native results,
or maintains a second todo ledger. Re-run the fail-closed live ownership probe
after a pinned protocol upgrade:
bun scripts/probe-tool-bridges.ts --case native-todo --model grok-4.5-fast --effort lowThe expected current-build outcome is a nonzero exit plus a sanitized
native-todo-sequence.json fixture documenting CURSOR_OWNS_TODO_STATE_NO_CLIENT_HANDOFF.
Develop locally
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 quality fixtures (including protocol-snapshot /
proto:check) alongside a parallel repository-gate phase: header/surface parity
plus seven Bun smoke groups (five-way concurrency). 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-proxy,
streaming-lifecycle, transport, utilities.
Logs
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.
