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 (
undicirequires this engine;@cursor/sdkneeds ≥ 22.13)
Authentication
Cursor auth does not use a browser OAuth CLI command. Resolve order:
- Provider
api_keythat starts withcrsr_(concrete key, not an unresolved$…/${…}placeholder) - Otherwise
CURSOR_API_KEYfrom 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_keyinconfig.json(notbaseUrl/apiKey) api_base_urlis 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", { … }].
| Option | Default | Description |
|---|---|---|
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. |
sandboxEnabled | false | Opt-in Cursor local sandbox. Forced off in Docker / unsupported hosts. Can also enable with CCR_CURSOR_SANDBOX=1 on a supported desktop host. |
Bridge mode (recommended for Claude Code)
- CCR starts / resumes an in-process Cursor agent session
- Host tools from the Claude Code request are registered as SDK custom tools
- When Cursor wants a tool, CCR parks the call and streams OpenAI-style
tool_callsback to Claude Code - Claude Code runs the tool and posts results; CCR resolves the parked promises and continues the stream
- 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.mdinside 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.
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 from
x-ccr-cursor-sessionheader, Claudemetadata.user_id(…_session_…), or a hash of model + system/first user text - 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/sdkAgent create/send/stream in-process - returns a ready
Responsevia__providerResponse(skips HTTPfetchto the provider URL) - emits OpenAI chat.completion / chat.completion.chunk SSE for AnthropicTransformer
- supports streaming and non-streaming Claude Code requests
- maps effort / reasoning fields onto SDK model selection when available
- 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()thinkingmessages andAgent.send({ onDelta })thinking-deltaupdates, then emits the Anthropic-compatible signature delta Claude Code expects - requires Claude Code's
"showThinkingSummaries": trueclient 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.sendand 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.forceis 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.