Skip to main content

Cursor SDK Integration

Claude Code Router can route Claude Code requests through Cursor models via the official @cursor/sdk package. Unlike HTTP providers, the cursor-sdk transformer owns the full upstream call in-process and returns OpenAI-compatible SSE/JSON that AnthropicTransformer converts back for Claude Code.

The default mode is bridge: Cursor chooses what to do next, but Claude Code remains the tool host. Cursor built-in tools are denied in the isolated workspace; host tools are exposed to the SDK as custom MCP tools (custom-user-tools).

Prerequisites​

  • A Cursor account with an API key starting with crsr_ (from the Cursor dashboard)
  • Claude Code Router running (Docker Compose or local)
  • Node.js ≥ 22.19.0 when running from source or publishing packages (undici requires this engine; @cursor/sdk needs ≥ 22.13)

Authentication​

Cursor auth does not use a browser OAuth CLI command. Resolve order:

  1. Provider api_key that starts with crsr_ (concrete key, not an unresolved $… / ${…} placeholder)
  2. Otherwise CURSOR_API_KEY from the environment

Recommended patterns:

"api_key": "crsr_your_key_here"

or keep the secret out of the file:

"api_key": "$CURSOR_API_KEY"

and export / inject the env var (Docker Compose already passes CURSOR_API_KEY into the container when set).

Setup​

1. Configure Provider​

Add a Cursor provider to ~/.claude-code-router/config.json:

{
"Providers": [
{
"name": "cursor",
"api_base_url": "https://cursor.com",
"api_key": "$CURSOR_API_KEY",
"models": ["composer-2", "claude-opus-4-8", "gpt-5.4"],
"transformer": {
"use": [
[
"cursor-sdk",
{
"cursorMode": "bridge"
}
]
]
}
}
],
"Router": {
"default": "cursor,composer-2"
}
}

Notes:

  • Use api_base_url / api_key in config.json (not baseUrl / apiKey)
  • api_base_url is a placeholder for provider identity; the SDK call does not use HTTP fetch to that URL
  • Discover the live model list with ccr model get cursor (see below)

2. Restart​

docker compose restart ccr
# or
ccr restart

Modes​

Pass options on the transformer entry: ["cursor-sdk", { … }].

OptionDefaultDescription
cursorMode"bridge"bridge — Claude Code hosts tools; Cursor builtins denied. plan — text/reasoning only (no tool execution). agent — Cursor agent mode; optional cursorCwd for local agent cwd.
cursorCwd(session workspace)Used when cursorMode is agent to set the SDK local cwd.
sandboxEnabledfalseOpt-in Cursor local sandbox. Forced off in Docker / unsupported hosts. Can also enable with CCR_CURSOR_SANDBOX=1 on a supported desktop host.
  1. CCR starts / resumes an in-process Cursor agent session
  2. Host tools from the Claude Code request are registered as SDK custom tools
  3. When Cursor wants a tool, CCR parks the call and streams OpenAI-style tool_calls back to Claude Code
  4. Claude Code runs the tool and posts results; CCR resolves the parked promises and continues the stream
  5. Deny-hooks in the isolated workspace block Cursor built-ins so filesystem/shell stay with Claude Code

Isolated workspaces live under:

~/.claude-code-router/cursor-sdk-workspaces/

Host environment grounding​

Cursor builds its harness prompt server-side from the SDK workspace root, so the model is told — at system level — that the isolated workspace is its project. When CCR runs in Docker that claim is self-consistent (Linux, /root/..., empty directory), while the user's real project lives on the Claude Code host. Models that treat system context as authoritative can conclude they are confined to the workspace and start prefixing tool paths with it.

To prevent that, bridge mode extracts the host's <env> block (project root, platform, OS version, and any other reported facts) from each incoming request and states the real topology — tools execute on a different machine — in three places:

  • AGENTS.md inside the workspace, which Cursor injects as project rules
  • the head and the tail of the prompt sent to the agent
  • the message returned when a Cursor built-in is denied

Host facts are re-read every turn, and the workspace files are rewritten only when the reported environment actually changes. Cursor loads workspace rules once per agent session, so a rewrite applies to the next session for that directory — a live turn always receives the current facts through the prompt itself. When a request carries no environment block, CCR never guesses a root — it falls back to instructing the model to use only absolute host paths that appear in the conversation.

warning

Do not set cursorCwd to the host project path when CCR runs in a container. The path is created if missing, which yields an empty phantom directory at the real project path inside the container.

Scratch-path detection​

Guidance is preventive, so bridge mode also checks the arguments of every host tool call. If any string argument references the scratch workspace — including inside a shell command such as cd <workspace> && ls — the call is not forwarded to Claude Code. The model receives a corrective tool result naming the offending argument and the real host root, and retries.

The correction applies at most three times per session; after that calls are forwarded unchanged, since a model that keeps insisting may be acting on an explicit user request about that path.

Detection is skipped entirely when the host project root itself lives under the scratch root. Occurrences are counted in the session metrics (scratchPathViolations, scratchPathCorrections), logged per call at warn, and summarized per turn:

cursor-sdk turn produced scratch-workspace tool paths

Grep for that message to compare model behaviour — this is the failure mode that affects strict system-prompt models but not Cursor-native ones.

Workspace lifecycle​

Scratch workspaces are removed when their session is disposed (idle TTL, LRU eviction, or explicit disposal). Directories left behind by a crash or kill are swept hourly once they are older than 24 hours. Both paths only ever remove a directory that is a direct child of the workspace root and whose name is a 32-character session key, so a cursorCwd supplied for agent mode is never touched.

Plan / agent modes​

  • plan — planning/chat assistant; do not execute tools
  • agent — Cursor agent with its own local tooling semantics; prefer bridge when you want Claude Code to own tools

Model Discovery​

Cursor models are listed through @cursor/sdk (not a REST /models URL):

ccr model get cursor

CCR detects a Cursor provider when the provider name is cursor or when transformer.use includes cursor-sdk. Auth for discovery uses the same crsr_ / CURSOR_API_KEY rules as the server.

After syncing models into config.json, restart so the running server picks up the list.

Sessions​

Cursor conversations are stateful in-process:

  • Session key is an opaque id CCR mints from the inbound protocol's own conversation id and stores under a hashed key in the session registry; conversation text is never persisted. Clients never send or receive x-ccr-cursor-session.
    • Anthropic Messages: metadata.user_id (session_id, a _session_ suffix, or the raw string), else x-claude-code-session-id.
    • Chat Completions: the client's own session header (x-opencode-session, x-kilocode-taskid, x-kilo-session, x-grok-session-id, x-grok-conv-id, x-conversation-id, session-id, x-session-id, x-session-affinity), else conversation / conversation_id, else prompt_cache_key.
    • Responses: those headers, else prompt_cache_key. The Responses conversation field is rejected, so it is not an identity.
    • A native id is combined with the first substantive user text. Side calls sent under the conversation id (OpenCode's title request, parallel Claude Code Tasks) get their own agent instead of superseding the main turn.
    • Worker forks with inherited history use their latest explicit fork marker (<fork-boilerplate> or You are a worker fork) as the identity boundary. The worker's own opening instructions and first assistant reply identify its agent, not the inherited parent turns. Shared boilerplate followed by different instructions separates sibling workers; the full inherited transcript still reaches the SDK. Nested contexts use a separate namespace from parent contexts.
    • No native id: the substantive user-text list (as prefix hashes). A later turn that extends it keeps the session; a sibling that only shares an earlier prefix does not. System text is not part of the id.
    • An opening (no assistant turn or tool result yet) that repeats a known one reuses its agent only as a retry: while its admitted turn is still running (including catalog lookup and agent creation) or within the 60-second replay window after it finished, and before any later turn. Otherwise it gets a fresh agent, so a stateless client never sees an earlier run's history.
    • Later turns are also keyed by the conversation's first assistant turn (its tool-call ids, else its text). A repeated opening therefore never takes over an ongoing conversation. When an identical opening replaced one that had no later turn yet, the two cannot be told apart, so the next later turn gets a fresh agent with a full transcript replay.
  • LRU cap of 32 sessions; idle TTL 15 minutes
  • In-flight sessions (live stream, running run, or parked tools) are not idle-evicted
  • If the stream dies mid-turn (disconnect / cancel), the next request uses a slim follow-up prompt when the agent session already has history

Running with Docker​

@cursor/sdk ships platform-native packages and is installed into the runtime image separately from the pnpm workspace (version taken from packages/server/package.json).

Ensure the container receives the key:

environment:
- CURSOR_API_KEY=${CURSOR_API_KEY}

Local sandboxing is disabled inside Docker even if requested.

Transformer Behavior​

The cursor-sdk transformer:

  • runs @cursor/sdk Agent create/send/stream in-process
  • returns a ready Response via __providerResponse (skips HTTP fetch to the provider URL)
  • emits OpenAI chat.completion / chat.completion.chunk SSE for AnthropicTransformer
  • supports streaming and non-streaming Claude Code requests
  • maps the request's reasoning onto SDK model selection. For parameterized models it always sends a full catalog preset variant, with the smallest context window and never fast mode (fast=false):
    • No reasoning requested (no thinking / effort / reasoning, effort none, or thinking disabled): each parameter's first allowed value, which is what Cursor runs for an omitted parameter and the least reasoning the model offers. Claude models get thinking=false, GPT 5.4+ reasoning=none. Grok 4.7 has no off switch, so it gets 256k / low / fast=false.
    • Reasoning with an effort: thinking=true where the model has it, and the nearest allowed effort level (xhigh matches GPT's extra-high; on a tie the lower level wins).
    • Reasoning without an effort (e.g. Anthropic adaptive thinking): the catalog default variant's effort (Grok 4.7: 256k / high / fast=false).
    • A failed catalog refresh keeps using the last catalog. A live agent keeps its bound selection while a run is in flight (a new reasoning choice applies to the next agent), and is never reminted just because the catalog could not be read.
  • keeps Cursor caching native to the SDK agent session, while exposing bounded cache-read usage back to Claude Code from SDK usage deltas
  • forwards Cursor thinking from both run.stream() thinking messages and Agent.send({ onDelta }) thinking-delta updates, then emits the Anthropic-compatible signature delta Claude Code expects
  • requires Claude Code's "showThinkingSummaries": true client setting for interactive display on Claude Code 2.1.89+; without it, CCR still transports the thinking block and Claude Code persists it, but the interactive UI hides the summary
  • classifies Claude Code's trailing turn once at the Anthropic boundary, using exact protocol markers rather than prompt-text regexes, and keeps that intent in request-local context instead of serializing it upstream
  • coalesces identical overlapping retries through one bounded, replayable response producer, so only one agent.send and one Cursor iterator consumer exist for a logical turn
  • treats the last-subscriber stop/interrupt as a real SDK cancellation and awaits bounded retirement before a replacement session can be created
  • resumes only a live parked run with an exact set of tool results and no meaningful steering; rejected-result plus replacement-text, unmatched/dead runs, cleanup failures, and transcript divergence retire the agent and replay the full transcript
  • reuses an idle agent with a slim prompt only when the next host transcript is the exact committed assistant text/tool calls plus one supported user message; larger suffixes are fully replayed, and local.force is never used as a substitute for lifecycle or transcript alignment

Usage​

{
"Router": {
"default": "cursor,composer-2",
"think": "cursor,claude-opus-4-8",
"background": "cursor,claude-haiku-4-5"
}
}

Troubleshooting​

Cursor API key not found: Set Providers[].api_key to a key starting with crsr_, or export CURSOR_API_KEY. Placeholders like $CURSOR_API_KEY only work when the env var is actually set.

Wrong key prefix: Cursor dashboard keys start with crsr_, not sk-.

Node engine errors: Local install / publish requires Node ≥ 22.19.0.

No models from ccr model get cursor: Confirm auth and that the provider uses cursor-sdk. Restart after writing models.

Tools run inside Cursor instead of Claude Code: Use cursorMode: "bridge" (default) and do not enable unsupported sandbox options that change hosting assumptions.

Session disposed / stream dropped under load: Sessions are capped (32) and idle-evicted after 15 minutes when not in flight. Prefer stable session headers for long conversations.