An opencode plugin that adds Cursor as a native provider. Your Cursor models appear in the model picker; you chat with them the same way you use any other provider.
It uses the official Cursor SDK (@cursor/sdk) to list your account's models live and run chats through Cursor's local agent runtime. For delegated or background workflows it also ships two permission-gated tools (cursor_delegate, cursor_cloud_agent) — see Delegation tools.
⚠️ Security. When you chat with acursor/*model, Cursor runs its own tools — includingshell,write,edit, anddelete— directly in your working directory, outside opencode's permission system. Read Security before you use it.
- opencode 1.17+
- Node.js 22.13+ on your
PATH(optional) — opencode runs on Bun and the plugin runs the Cursor SDK in-process by default; Node is only needed for thesidecartransport fallback (see Transport). - A Cursor account and API key (from the Cursor dashboard).
curl -fsSL https://raw.githubusercontent.com/stablekernel/opencode-cursor/main/install.sh | bashRegisters the plugin in your global opencode.json (~/.config/opencode/opencode.json), checks
for Node.js 22.13+, and offers to set CURSOR_API_KEY. Flags:
--project— write./opencode.jsonin the current directory instead.--yes/-y— non-interactive.
npm install @stablekernel/opencode-cursorAdd to your opencode.json (or opencode.jsonc — both are supported):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@stablekernel/opencode-cursor@latest"]
}The @latest suffix makes opencode re-resolve to the newest release on each
startup. In practice opencode caches the @latest plugin install and does not
auto-update it. If you see a stale-version warning from the plugin, or a version
mismatch, exit opencode and clear the cached package:
# macOS / Linux
rm -rf ~/.cache/opencode/packages/@stablekernel/opencode-cursor@latest
# Windows
rmdir /s /q "%LocalAppData%\opencode\cache\packages\@stablekernel\opencode-cursor@latest"Then restart opencode.
Drop @latest ("@stablekernel/opencode-cursor") or pin a version
("@stablekernel/opencode-cursor@1.2.3") if you prefer deterministic installs.
The stale-version check is skipped when the CI or NO_UPDATE_NOTIFIER
environment variable is set.
To keep the plugin up to date easily, install the opencode-plugins-refresh helper (offered by
the one-line installer, or install manually — see Keeping the plugin up to date).
The plugin injects the provider block automatically. If you need explicit control:
{
"provider": {
"cursor": {
"npm": "@stablekernel/opencode-cursor",
"name": "Cursor",
"options": { "apiKey": "{env:CURSOR_API_KEY}" }
}
}
}opencode pins @latest plugins on first install and never auto-updates them. When the installed
version falls behind the latest release, the plugin shows a warning once every 24 hours at startup.
The warning tells you what to do:
- If
opencode-plugins-refreshis on yourPATH, the warning saysrun: opencode-plugins-refresh. - Otherwise it shows the raw
rm -rfcommand and suggests re-running the installer to get the helper script.
opencode-plugins-refresh is a shell script that checks all @latest plugin caches for updates
by comparing pinned versions against npm, and optionally clears outdated caches so opencode
re-fetches the latest on next launch.
opencode-plugins-refresh # check for updates, prompt to clear cache
opencode-plugins-refresh --check # check only, exit 1 if outdated
opencode-plugins-refresh --force # clear all outdated caches without promptingThe --check flag exits with code 1 when any cache is outdated, making it suitable for CI jobs
or cron checks.
The one-line installer offers to install opencode-plugins-refresh to ~/.local/bin (step 4).
To install it manually at any time:
curl -fsSL https://raw.githubusercontent.com/stablekernel/opencode-cursor/main/scripts/opencode-plugins-refresh \
-o ~/.local/bin/opencode-plugins-refresh && chmod +x ~/.local/bin/opencode-plugins-refreshMake sure ~/.local/bin is on your PATH.
opencode auth login # choose "Cursor", paste your key from the Cursor dashboardOr set the environment variable:
export CURSOR_API_KEY="key_..."The key is validated on first use (model discovery / first call), not at login time.
opencode models(or the model picker) lists your Cursor models ascursor/<id>.- Pick a model and chat — the Cursor local agent runs in your project directory.
- Run the
cursor_refresh_modelstool to force a live catalog refresh.
The plugin also registers two delegation tools:
cursor_delegate— hand a discrete subtask to a local Cursor agent as a permission-gated tool call (your primary model stays in control).cursor_cloud_agent— launch a Cursor cloud agent on a remote repo that can run for minutes and optionally open a PR.
⚠️ The provider path is unsandboxed and not gated by opencode permissions. When you chat with acursor/*model, Cursor runs its own tools — includingshell,write,edit, anddelete— directly in your working directory. opencode'spermissionrules (e.g.edit: deny,bash: ask) do not apply to them.Options if you need a permission boundary:
- Set
sandbox: trueinprovider.cursor.optionsto run Cursor's tools in Cursor's sandbox.- Use
cursor_delegateinstead of the provider path — it is gated by opencode'spermissionconfig.- By default opencode's system prompt is delivered via a git-ignored Cursor rule (
systemPrompt: "rules"), not inlined into the message stream. See System prompt.
See SECURITY.md for the full threat model.
Option (provider.cursor.options) |
Default | Meaning |
|---|---|---|
apiKey |
CURSOR_API_KEY |
Cursor API key |
cwd |
process.cwd() |
Directory the local agent operates in |
mode |
"agent" |
Default conversation mode ("agent" or "plan") |
params |
— | Default model params, e.g. { thinking: "high" } |
settingSources |
— | Cursor settings layers to load: ["project","user","all",...] — pulls in your Cursor skills, rules, and .cursor/mcp.json |
sandbox |
— | Run the agent's tools in Cursor's sandbox |
autoReview |
false |
Gate tool calls through Cursor's classifier-backed Auto review (best-effort, not a security boundary) |
agents |
— | Cursor subagent definitions |
session |
"auto" |
Session reuse strategy — see Session reuse |
forwardMcp |
true |
Forward opencode's configured MCP servers to the Cursor agent |
mcpServers |
— | Extra MCP servers (Cursor McpServerConfig shape); merged with forwarded ones |
forwardSkills |
true |
Mirror opencode's resolved skills into .cursor/skills/ — see Skills |
skills |
— | Manual skill override: { include?: string[], exclude?: string[] } — see Skills |
toolDisplay |
"blocks" |
How Cursor's internal tool activity is shown — see Tool display |
systemPrompt |
"rules" |
How opencode's system prompt reaches the agent — see System prompt |
transport |
— | Cursor agent transport ("http1" | "http2-direct" | "sidecar") — see Transport |
autoCompaction |
false |
Let opencode drive auto-compaction. Off by default because the Cursor agent self-compacts — see Compaction |
| Environment variable | Default | Meaning |
|---|---|---|
CURSOR_API_KEY |
— | API key fallback |
OPENCODE_CURSOR_MODEL_CACHE_TTL_MS |
86400000 |
Model-list cache lifetime (ms) |
OPENCODE_CURSOR_DEBUG |
— | Set to 1 for trace logging on stderr |
OPENCODE_CURSOR_TRANSPORT |
— | Force a transport: http1 | http2-direct | sidecar — see Transport |
OPENCODE_CURSOR_STALL_MS |
120000 |
Idle stream-watchdog timeout in ms (no tool call open). 0 disables the whole watchdog; an empty string also disables — see Reliability |
OPENCODE_CURSOR_TOOL_STALL_MS |
600000 |
Stream-watchdog timeout in ms while a tool call is in flight (e.g. a long build or test suite). 0 disables the bound during tool execution only — see Reliability |
OPENCODE_CURSOR_SIDECAR |
— | Legacy: 1 maps to sidecar, 0 maps to http2-direct (superseded by OPENCODE_CURSOR_TRANSPORT) |
OPENCODE_CURSOR_TOOL_INPUT_STREAM |
on | Set to 0 to disable live tool-input streaming (tool-input-start/-delta/-end parts) |
opencode re-sends the full conversation transcript on every turn. session: "auto" (the default)
fingerprints the conversation and resumes the same Cursor agent when nothing has changed, so you
only pay for the new message. It falls back to a fresh agent + full transcript on edits, reverts,
or compaction.
| Situation | What happens |
|---|---|
| First turn | Fresh agent, full transcript, pool it |
| System prompt differs (title gen, other side calls) | Ephemeral fresh agent; pooled agent untouched |
| Clean continuation (one new user message) | Agent.resume — sends only the new message |
| Forwarded MCP server set changed | Fresh agent + full transcript, re-pooled |
| Message edited/reverted or conversation compacted | Fresh agent + full transcript, re-pooled |
session: true is an alias for "auto". session: false disables reuse (always fresh agent,
full transcript every turn).
Fingerprint records persist to ~/.cache/opencode-cursor/session-pool.json, so session reuse
survives opencode restarts.
The plugin auto-generates model variants for each reasoning/effort level a model advertises,
plus a fast toggle for models that expose Cursor's fast tier. Selecting a variant in the
model picker sends its settings through providerOptions.cursor.
fast defaults off (even though Cursor's own default is fast: true for some models, e.g.
Composer and the codex line) so opencode never silently runs the fast tier — pick the fast
variant to opt in, or set it per model under options.params.fast below.
opencode's plan agent (Tab) maps to Cursor's plan mode automatically — no manual config
needed.
To set controls statically per model:
{ "provider": { "cursor": { "models": {
"composer-2.5": { "options": { "params": { "thinking": "high" } } }
} } } }opencode drives the Cursor agent the way it drives any provider — through its system prompt. But the Cursor SDK has no system-prompt input (an agent, not a raw model), and flattening opencode's system prompt into the message stream makes injection-hardened Cursor models reject it as a prompt-injection attempt.
So by default (systemPrompt: "rules") the plugin writes opencode's system prompt
to <cwd>/.cursor/rules/opencode.mdc (alwaysApply: true, git-ignored) and loads
the project settings layer, delivering it through Cursor's authoritative rules
channel. Cursor treats rules as system-level instructions, so opencode stays in
control and nothing is flagged.
Tradeoffs to know:
- A project rule also applies to your own Cursor IDE open on this repo. The plugin removes the file when the session disposes (best-effort).
- Enabling the
projectlayer also loads other.cursor/config (.cursor/mcp.json,.cursor/agents, hooks).
Alternatives:
systemPrompt: "message"— legacy inline delivery (may be rejected as injection).systemPrompt: "omit"— don't forward the system prompt at all.
With forwardMcp: true (default), the Cursor agent uses the same MCP servers configured in
opencode. The server list is updated live per turn, so enabling or disabling an MCP server takes
effect on the next message.
opencode config.mcp |
→ Cursor |
|---|---|
{ type: "local", command: [cmd, ...args], environment } |
{ type: "stdio", command, args, env } |
{ type: "remote", url, headers } |
{ type: "http", url, headers } |
Remote with registered OAuth clientId |
{ type: "http", url, auth: { CLIENT_ID, … } } |
Disabled entries (enabled: false) are skipped. Remote servers requiring OAuth without a
shareable clientId are also skipped (a one-time toast says which). Disable forwarding with
forwardMcp: false.
Note: This forwards MCP servers. opencode's skills are mirrored into
.cursor/skills/automatically — see Skills.
With forwardSkills: true (default), the plugin mirrors opencode's resolved
skills into <cwd>/.cursor/skills/ — a git-ignored directory that Cursor
discovers natively when the project settings layer is loaded. This works for
both the primary agent and any Cursor sub-agent, because the mirror is written
at plugin init (before any turn) and settingSources lives on the provider
config (which survives the sub-agent model-options drop).
The mirror includes:
- Project skills from
.opencode/skills/,.opencode/skill/,.claude/skills/,.agents/skills/(walked up to the git worktree root). - Global skills from
~/.config/opencode/skills/and~/.config/opencode/skill/,~/.claude/skills/,~/.agents/skills/,~/.opencode/skills/and~/.opencode/skill/. - Configured paths from
config.skills.pathsin youropencode.json— additional directories scanned at the lowest priority (project and standard global locations win on duplicate ids).~/prefixes are expanded to your home directory; relative paths are resolved against the project directory. - Supporting files alongside each
SKILL.md(preserving relative paths). - An
<available_skills>catalogue appended to the generated system rule, listing each skill's id and description so the Cursor agent can load them on demand.
Note:
config.skills.urls(HTTP skill catalogs) are not yet supported by the mirror. If you rely on URL-sourced skills, they will not appear in.cursor/skills/.
Skills are filtered through opencode's live permission config before
mirroring:
allow(default): included in the mirror.deny: excluded entirely.ask: excluded — the ask prompt can't be enforced across the Cursor boundary. The plugin logs which skills were withheld and why.
{
"provider": {
"cursor": {
"options": {
"skills": {
"include": ["my-skill", "other-skill"],
"exclude": ["internal-*"]
}
}
}
}
}include keeps only the listed skills (and overrides deny permission — the
user explicitly asked for them). exclude always drops the listed skills.
- Skills bundled inside opencode plugins are not mirrored. Those ship under
<opencode-package-cache>/<pkg>/node_modules/<pkg>/skills/(on macOS/Linux~/.cache/opencode/packages/; on Windows%LocalAppData%\opencode\cache\packages\), which is not a scanned location —@opencode-ai/sdkexposes no skills API, so the mirror resolves skills from the filesystem itself. Skills that reach opencode only through a plugin will be absent from.cursor/skills/. To mirror one, add its directory toconfig.skills.paths. - A user-owned
.cursor/skills/<id>/SKILL.md(without thegenerated: opencode-cursorsentinel) is never overwritten or deleted. - Individual files larger than 1 MB are skipped (the rest of the skill is still mirrored). Total mirror size is capped at 10 MB.
cursor_delegatewith acwddifferent from the session directory does not mirror skills into that cwd (but does passsettingSources: ["project"]so any pre-existing.cursor/skills/there still loads).cursor_cloud_agenttargets a remote repo and does not inherit skills.
Both tools resolve the API key from your opencode auth login session (or CURSOR_API_KEY) and
are gated by opencode's permission config:
{ "permission": { "cursor_delegate": "ask", "cursor_cloud_agent": "ask" } }Runs one Cursor turn as a permission-gated tool call. Your primary opencode model hands off a
discrete subtask and gets the result back. The delegate passes settingSources: ["project"] so
any pre-existing .cursor/skills/ in the delegate's cwd loads (skills are not mirrored into a
non-session cwd — see Skills limitations).
| Arg | Required | Meaning |
|---|---|---|
prompt |
✅ | The subtask to delegate |
model |
✅ | Cursor model id |
mode |
— | "agent" or "plan" |
thinking |
— | Thinking level (e.g. "high") |
cwd |
— | Working directory |
sandbox |
— | Run in Cursor's sandbox |
agentId |
— | Resume a specific Cursor agent |
Launches a background Cursor cloud agent on a remote repo. Can run for minutes and optionally open a PR.
| Arg | Required | Meaning |
|---|---|---|
prompt |
✅ | The task |
repoUrl |
✅ | Target repository URL (e.g. https://github.com/owner/repo) |
startingRef |
— | Branch/ref to start from |
model |
— | Cursor model id |
mode |
— | "agent" or "plan" |
thinking |
— | Thinking level |
autoCreatePR |
— | Open a PR when finished |
workOnCurrentBranch |
— | Operate on the current branch instead of a new one |
toolDisplay controls how Cursor's internal tool activity appears in opencode:
"blocks"(default) — structured, collapsible tool blocks with inputs and outputs. Common Cursor tools are mapped to their opencode equivalents (edit→ diff viewer,shell→ bash console, etc.). Requires opencode 1.17+."reasoning"— compact inline lines ([tool] write {"path":…}). Works on any host; use this on older opencode versions.
Turns where the host declares no tools at all — compaction/summary and title generation — always
use "reasoning" regardless of this setting. opencode rejects tool parts on a summary turn, so
Cursor's tool activity is folded into reasoning text there instead.
To force the fallback:
{ "provider": { "cursor": { "options": { "toolDisplay": "reasoning" } } } }opencode's threshold-triggered auto-compaction is suppressed for Cursor models by default. The Cursor agent runtime compacts its own conversation as it approaches its context threshold, so a second, opencode-driven pass is redundant — and it is actively harmful here:
- The compaction turn asks the model to summarize with no tools available. The Cursor agent runs
its own tools regardless, which opencode rejects outright
(
Tool call not allowed while generating summary). - Compaction rewrites the transcript, so the next turn no longer matches what the Cursor agent saw. The plugin correctly treats that as a divergence and creates a fresh Cursor agent — and every distinct agent permanently holds its own SQLite store open for the life of the process, which has been observed to crash opencode outright.
The suppression works by emitting a very large limit.input, which is what opencode uses as its
compaction threshold. The real limit.context is left untouched, so the TUI's context-window gauge
and cost reporting keep working.
Important
This suppresses the proactive threshold trigger only, and opencode has no reactive
context-overflow recovery wired up for this provider. In exchange, opencode's transcript is no
longer trimmed automatically, so it grows for the life of the session. Ordinary turns send only
the new message to an already-running agent, but a cold replay — a new session, an expired
agent, or a changed MCP server set — resends the whole transcript. If that ever overflows the
model, the turn fails with a provider error and the fix is to run /compact manually.
Set autoCompaction: true if you would rather have opencode keep bounding the transcript for you.
Manual /compact is unaffected and still works — it has no threshold gate.
To hand compaction back to opencode:
{ "provider": { "cursor": { "options": { "autoCompaction": true } } } }opencode runs on Bun, whose node:http2 client is incompatible with the Cursor
SDK's long-lived streaming RPC (NGHTTP2_FRAME_SIZE_ERROR; see
oven-sh/bun#31499). The plugin works around this by
running the SDK over HTTP/1.1 in-process — no Node child process required. The historical Node
sidecar remains as a rollback fallback.
| Transport | Where it runs | When it's the default |
|---|---|---|
http1 |
in-process, HTTP/1.1 + SSE (Bun-safe) | under Bun |
http2-direct |
in-process, SDK's default HTTP/2 | under Node (tests, scripts, non-Bun hosts) |
sidecar |
spawned Node child hosting the SDK | never (rollback only) |
Resolution order: the transport provider option → OPENCODE_CURSOR_TRANSPORT →
legacy OPENCODE_CURSOR_SIDECAR (1→sidecar, 0→http2-direct) → the per-runtime default
above.
If you hit a regression on the in-process path, roll back to the sidecar:
export OPENCODE_CURSOR_TRANSPORT=sidecar # requires Node.js 22.13+ on PATHAn explicit sidecar request with no Node on PATH falls back to http1 (Bun) or http2-direct
(Node) with a stderr notice.
The provider classifies Cursor SDK errors into typed kinds (agent-not-found, agent-busy,
rate-limit, network, auth, config, unknown) and recovers per kind:
- agent-busy — a previous crash left a run wedged; the send is retried once with the SDK's
local.forceescape hatch. - rate-limit / network — bounded exponential backoff on the same agent.
- auth / config — fail fast (not retried).
Sends carry an idempotency key so a retry is a server-side dedupe, not a duplicate turn.
A stream watchdog guards against a wedged run that streams nothing. It uses two budgets:
- Idle (
OPENCODE_CURSOR_STALL_MS, default120000): when no tool call is open. A pre-first-event stall cancels and force-resends once; a stall after partial output is surfaced as a terminal error rather than re-emitting the already-yielded prefix. - Tool-phase (
OPENCODE_CURSOR_TOOL_STALL_MS, default600000): while at least one Cursor tool call is in flight. A long shell command, build, or test suite legitimately streams nothing for minutes; the larger budget stops a healthy run from being killed mid-tool. A tool-phase stall is terminal and names the in-flight tool. Set0to disable the bound during tool execution only.
The watchdog re-arms on any SDK update — including types the plugin doesn't model — so
progress/heartbeat updates count as liveness. Set OPENCODE_CURSOR_STALL_MS=0 to disable the whole
watchdog (an empty string also disables, for backward compatibility).
- Native Cursor tools hang / "Tool execution aborted" (
NGHTTP2_FRAME_SIZE_ERROR). Ahttp2-directtransport was forced under Bun. UnsetOPENCODE_CURSOR_TRANSPORT(defaults to the Bun-safehttp1), or roll back withOPENCODE_CURSOR_TRANSPORT=sidecar(needs Node.js 22.13+ onPATH). - Plugin enabled but no
cursorprovider/models appear, or you see a stale-version warning. opencode caches the@latestplugin install on first use and never refreshes it. Exit opencode, delete~/.cache/opencode/packages/@stablekernel/opencode-cursor@latest(or the pinned version directory), and restart. - Only the four fallback models appear. The live catalog loads after the first authenticated
use. Restart opencode once after login, or run
cursor_refresh_models. - Invalid or expired key. Validated on first use — that's where the error surfaces.
- Need more detail? Set
OPENCODE_CURSOR_DEBUG=1.
Issues and pull requests are welcome. See CONTRIBUTING.md for dev setup, test/typecheck/build commands, and the release process. Report bugs at the issue tracker; for security reports see SECURITY.md.
MIT