An OpenClaw CLI-backend plugin that registers the
Cursor Agent CLI (cursor-agent) as a model
provider — cursor-cli/<model>.
OpenClaw's bundled acpx plugin already lets an agent spawn Cursor as a
coding worker via ACP. That's a different thing from what this plugin does:
this lets an agent's own reasoning — the part that reads a message and
decides what to do — run on a Cursor subscription, the same way
claude-cli lets it run on a Claude Code subscription.
The practical use case is resilience: if you have both a Claude and a
Cursor subscription, you don't have to be locked into one running out of
usage. Add cursor-cli/<model> as a fallback (agents.list.<id>.model.fallbacks)
alongside your primary, and OpenClaw fails over automatically.
- Cursor Agent CLI installed and logged in
(
cursor-agent login) on the same host as the OpenClaw gateway. - OpenClaw with plugin support (any recent version — built against the
cliBackends/registerCliBackendplugin API).
openclaw plugins install /path/to/openclaw-cursor-cli
openclaw plugins enable cursor-cli(Or point openclaw plugins install at this repo once it's published —
git URL or npm spec both work with OpenClaw's plugin installer.)
Restart the gateway after enabling — CLI-backend registration needs a restart to take effect, same as any other plugin enable/disable.
As installed above, cursor-cli/<model> will fail every request with
Unknown model: cursor-cli/<model> (or, if you've hit the variant of this
bug, No API provider registered for api: ...). This isn't something
register(api) can fix from inside the plugin — traced it live, with
instrumented debug logging, all the way to OpenClaw's own
resolveRuntimeCliBackends() (model-selection-*.js), which reads from
loadPluginRuntime()?.getActivePluginRegistry()?.cliBackends. For a
bundled CLI backend (claude-cli, google-gemini-cli) that registry is
populated correctly. For a third-party/linked plugin — this one, and
presumably any other non-bundled registerCliBackend plugin — it comes back
empty at model-resolution time, even though openclaw plugins inspect
correctly shows the backend as loaded. The bundled-only fallback path
(resolveBundledSetupCliBackends) makes this explicit: it filters
plugin.origin === "bundled", which a linked/npm-installed plugin never is.
The fix is a second, independent lookup path OpenClaw does support:
statically declaring the backend's config directly under
agents.defaults.cliBackends, which resolveCliBackendConfig consults
before falling back to the (broken, for third-party plugins)
registry-based lookup. Add this once, after installing the plugin:
openclaw config set 'agents.defaults.cliBackends' '{
"cursor-cli": {
"command": "cursor-agent",
"args": ["--print", "--force", "--output-format", "stream-json"],
"resumeArgs": ["--print", "--force", "--output-format", "stream-json", "--resume", "{sessionId}"],
"output": "jsonl",
"jsonlDialect": "claude-stream-json",
"input": "stdin",
"modelArg": "--model",
"modelAliases": { "auto": "auto" },
"sessionMode": "existing",
"sessionIdFields": ["session_id"],
"clearEnv": ["CURSOR_API_KEY", "CURSOR_API_ENDPOINT"],
"serialize": true,
"reliability": {
"watchdog": {
"fresh": { "noOutputTimeoutRatio": 0.8, "minMs": 180000, "maxMs": 600000 },
"resume": { "noOutputTimeoutRatio": 0.3, "minMs": 60000, "maxMs": 180000 }
}
}
}
}' --strict-json(If you already have other entries under agents.defaults.cliBackends,
merge rather than overwrite — this sets the whole map.) Restart the gateway
after. This must stay in sync with src/index.js's buildCursorCliBackend()
— if you change one, change the other. Verified live end-to-end after
applying this: real cursor-agent subprocess spawn, correct output, and
session --resume correctly reusing context across turns.
Optional plugin config, if cursor-agent isn't on the gateway's PATH:
{
plugins: {
entries: {
"cursor-cli": {
enabled: true,
config: { command: "/absolute/path/to/cursor-agent" }
}
}
}
}Set it as a primary or fallback model for any agent, scoped per-agent
(don't use the --agent flag on openclaw models set/fallbacks add —
it doesn't scope the write the way you'd expect; edit the agent's config
path directly):
openclaw config set 'agents.list[N].model.fallbacks' '["cursor-cli/auto"]' --strict-jsonModel ids pass straight through to cursor-agent --model. Run
cursor-agent --list-models for the current catalog (it's large and moves
fast — Claude, GPT, Gemini, Grok, Kimi, GLM variants, each with several
effort tiers) — anything in that list works directly as cursor-cli/<id>.
cursor-cli/auto (Cursor's own auto-routing) is the default and the safest
choice if you don't want to hardcode a specific model that might age out.
cursor-agent --print --force --output-format stream-json emits
newline-delimited JSON events shaped like Claude's own stream-json protocol
(type/subtype, message.content[], a final result event with the
answer text and token usage) with a session_id field on every line. This
plugin declares that shape to OpenClaw's generic CLI-backend machinery —
it's a small declarative config, not a custom parser.
One real difference from claude-cli: cursor-agent assigns its own session
id on the first call (there's no flag to hand it one up front) and only
accepts --resume <id> on later calls. That's sessionMode: "existing"
here, versus "always" for claude-cli.
- Requires the
agents.defaults.cliBackendsconfig step above. Not optional, not this plugin's bug — see "Required extra step" under Install for the root cause (OpenClaw's runtime CLI-backend registry doesn't pick up third-party/linked plugins the same way it picks up bundled ones). - No daemon/server mode. Unlike OpenAI's Codex (which ships an
app-serverprotocol specifically for external tools like this),cursor-agenthas no persistent process another tool can talk to — every turn is a fresh subprocess spawn, same asclaude-cli. Works fine, just means no protocol-level session pinning beyond--resume. --forceauto-approves everything within the run (matches the same risk posture OpenClaw's bundledclaude-clibackend already uses via--permission-mode bypassPermissions) — this is a reasoning backend for an already-trusted agent, not a sandboxed execution mode.- Model aliases are intentionally minimal. Cursor's model catalog is
large and changes often; hardcoding friendly names beyond
autowould go stale. Use literal model ids fromcursor-agent --list-models. - Auth is whatever
cursor-agent loginalready set up on the host — this plugin doesn't manage credentials itself. Concretely:registerProvider'sautharray is empty andresolveSyntheticAuthreturns a placeholder token purely so OpenClaw's "is this provider authenticated" check passes — no credential is ever read, stored, or exchanged by OpenClaw. - Both
registerCliBackendandregisterProviderare required for acursor-cli/<model>ref to resolve at all — this wasn't obvious going in and is worth calling out for anyone extending this plugin. OpenClaw keeps two separate plugin registries:registerCliBackendonly wires up how to run a model once one has already resolved (the subprocess command/args/ session handling below);registerProvider(specifically itsresolveDynamicModelhook) is what makescursor-cli/<model>resolve to a model object in the first place. A CLI backend with no matching provider fails every request with "Unknown model", regardless ofagents.defaults.modelsallowlist entries (that's a separate permission gate, not model resolution). Every bundled CLI-backend plugin (claude-cli,google-gemini-cli, Codex's app-server) registers both together for this reason.
MIT