Cursor IDE Integration#
Co-Vibe supports Cursor (1.7+, the hooks-capable releases) in three ways:
covibe-local setupwrites a repo-local.cursor/mcp.jsonentry so Cursor agents can call the Co-Vibe tools.- Setup also writes a repo-local
.cursor/hooks.jsonso the Co-Vibe hook (scripts/cursor-covibe-hook.ts) receives Cursor's session lifecycle. - The hook (on stop/sessionEnd) and
covibe-local watchsweep usage out of Cursor's agent transcripts (full 4-way token split) and its local sqlite store (legacy bubble tokens plus Cursor's own cost accounting).
What Setup Writes#
When setup detects Cursor — a ~/.cursor directory, or the directory named by
COVIBE_CURSOR_HOME — it merges the Co-Vibe server into .cursor/mcp.json:
{
"mcpServers": {
"covibe": {
"command": "node",
"args": ["<package>/bin/covibe-mcp.mjs"],
"cwd": "/path/to/repo",
"env": {
"COVIBE_AGENT": "cursor",
"COVIBE_BASE_URL": "http://localhost:3000",
"COVIBE_MCP_TOKEN": "${env:COVIBE_MCP_TOKEN}"
}
}
}
}and the hook command into .cursor/hooks.json (schema version: 1, each event
maps to an array of {command} entries):
{
"version": 1,
"hooks": {
"sessionStart": [{ "command": "node <package>/bin/covibe-cursor-hook.mjs" }],
"afterFileEdit": [{ "command": "..." }],
"stop": [{ "command": "..." }],
"sessionEnd": [{ "command": "..." }]
}
}Merges preserve every existing server and hook entry and are idempotent;
afterFileEdit is currently a no-op success reserved for per-edit overlap
checks. The hook payload arrives as JSON on stdin with conversation_id
(equal to the composerId in Cursor's store), generation_id, model,
hook_event_name, workspace_roots, and transcript_path; Cursor also sets
CURSOR_PROJECT_DIR.
Token Safety#
The raw cv_ token is never written to disk. Cursor interpolates
${env:NAME} placeholders in mcp.json command, args, env, url, and
headers values for stdio servers (its own syntax — not the bare ${NAME}
Claude Code expands in .mcp.json), so the config references
${env:COVIBE_MCP_TOKEN} and Cursor reads the token from your shell at spawn
time. Source: the Cursor MCP reference at cursor.com/docs/mcp (config
interpolation).
covibe-local doctor fails if a raw token ever appears in .cursor/mcp.json.
Exporting COVIBE_MCP_TOKEN is optional since the device-flow setup: the
non-secret COVIBE_AGENT: "cursor" marker lets the stdio bridge and the
Cursor hook look up the typed cursor token from ~/.covibe/credentials.json
when the shell variable is unset (Cursor passes the unexpanded placeholder
then, which the bridge treats as absent). An exported COVIBE_MCP_TOKEN
keeps precedence. See device-setup.md.
What Is Captured#
- Per-session per-model token usage with the full 4-way split (transcript
lane): agent-mode sessions write Claude-Code-shaped transcripts whose
assistant messages carry
message.usagewithinput_tokens,output_tokens,cache_read_input_tokens, andcache_creation_input_tokens; the sweep emits one event per (session, model) increment withcache_read_tokens/cache_write_tokensmapped from the cache fields,source_event_idcursor-transcript:<sessionId>:<model>:<usageLineCount>. - Per-bubble token counts (legacy composer fallback): each assistant message
is a
bubbleId:<composerId>:<bubbleId>row in the global store withtokenCount.inputTokens/outputTokens; one telemetry event per nonzero bubble,source_event_idcursor-bubble:<composerId>:<bubbleId>. When a composer id also has transcript usage, its bubble token events are suppressed — the transcript wins because it carries the full split, and emitting both would double count. Cost events are unaffected by this rule: they are Cursor's own cost accounting, stored as submitted cost rather than token-derived. - Per-composer per-model cost:
composerData:<composerId>rows carry ausageDatamap of model name to{costInCents, amount}; a change emits one cost event (cursor-usage:<composerId>:<model>,cost_usd = costInCents/100, zero token counts). - Lines added/removed: the hook pins the session's baseline HEAD at
sessionStart and submits the session-scoped git delta at stop/sessionEnd
(Cursor's own
totalLinesAdded/Removedcomposer counters stay on disk as a cross-check). - Session lifecycle: sessionStart/stop/sessionEnd hooks drive
covibe_sessionstart/snapshot/end, exactly like the Claude Code hook. - preCompact context stats: the payload carries
context_tokensandcontext_window_size; the hook recordsmax_context_tokenswhen thepreCompactevent is wired intohooks.json.
Provider is derived from the model name (claude* is anthropic; gpt*,
codex*, o<digit>* are openai; anything else is left unset).
Nothing above is captured for repos excluded with covibe-local exclude: the
hook exits before any tool call and the usage sweeps are repo-scoped (see
device-setup.md). For other repos, hook activity registers the repo in
~/.covibe/repos.json so the machine-level watch service picks it up
automatically — no per-repo registration command.
The Transcript Lane#
Current Cursor versions write agent-mode session transcripts at
~/.cursor/projects/<project-key>/agent-transcripts/<session_id>/<session_id>.jsonl
plus subagent files under .../<session_id>/subagents/agent-<id>.jsonl
(COVIBE_CURSOR_PROJECTS overrides the projects directory). The lines are
Claude-Code shaped — {"role": ..., "message": {"usage": ..., "model": ...}}
with a top-level role instead of type — so the sweep reuses the Claude
Code transcript parser (scripts/local-runtime/claude-usage.ts).
The <project-key> is the workspace path with path separators replaced by
-, colons dropped, and the leading - stripped (/Users/alice/proj ->
Users-alice-proj). Derivation varies slightly by Cursor version (e.g. the
drive-letter case on Windows), so when the derived key's directory is absent
the sweep scans every project directory and keeps the ones whose key still
prefix-matches case-insensitively. As a second net, the hook records each
payload's transcript_path (keyed by conversation_id) into the usage state
so the sweep reads exactly that file even when the key derivation misses.
The transcript session_id equals the hook's conversation_id and the
composerId used by the bubble lane, which is what makes the double-count
suppression above possible. Per-file line offsets and a per-session running
count of usage-bearing lines live in the usage state (the count keeps
source_event_ids stable across re-reads and monotonic as transcripts grow);
the transcript lane primes on its own first run, so installs upgrading from
the bubble lane do not flood historical transcript usage. The hook's stop
payload is officially only {status, loop_count} and is never read for
tokens.
How The Store Is Read#
The usage sweep reads
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
(COVIBE_CURSOR_DB overrides; Linux ~/.config/Cursor/..., Windows
%APPDATA%\Cursor\...). The database is WAL and locked while the IDE runs, so
every read opens an immutable=1 file: URI snapshot — never a lock.
Composers are scoped to the repo via the workspace join: each
workspaceStorage/<md5>/workspace.json names the opened folder, and that
directory's own state.vscdb lists the folder's composers under the
ItemTable key composer.composerData (subdirectory cwds are accepted).
Dedupe state lives in .covibe/cursor-usage-state.json (version 1): the first
sweep primes existing rows without submitting, and every event is keyed by its
source_event_id so re-runs never double-count.
Auto Model Resolution#
When the user runs Auto, the disk rows store the literal model "default".
The hook payload's model field is the only reliable resolved value, so the
hook records a conversation_id to model map into the usage state file and
the sweep uses it as the final fallback before "unknown". Sessions that ran
before the hook was installed cannot be resolved retroactively.
What Is Provably Uncapturable#
- The full 4-way token split for legacy composer history: transcripts capture
the complete split for new agent-mode sessions, but sessions that predate
the transcript layout only have bubbles, and roughly 88 percent of bubbles
carry no
tokenCountat all (so the remaining ~12 percent limitation applies to that history alone; the Cursor team Admin API is the only other source of complete splits for it). - Retroactive Auto-model resolution: disk stores
"default"; only the live hook payload knows the resolved model. - Per-request API latency: the store keeps timestamps per bubble, not per upstream request.
- Tab/autocomplete tokens: never written to the local store.
- Rate-limit/quota state: server-side only, not mirrored locally.
Doctor Check#
covibe-local doctor prints one cursor check:
INFOwhen Cursor is not installed (nothing to do)WARNwith a setup hint when Cursor is installed but.cursor/mcp.jsonis missing, missing the Co-Vibe server, or staleFAILwhen.cursor/mcp.jsonstores a raw tokenPASSwhen the config is wired (plus hook wiring viahasCoVibeCursorHooksand whether the globalstate.vscdbexists yet for the usage sweep)
Verification#
npx vitest run tests/unit/local-companion-cursor-setup.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-doctor.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-usage.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-transcripts.test.ts --no-file-parallelism
npx vitest run tests/unit/cursor-hook.test.ts --no-file-parallelismFile-Overlap Visibility#
Cursor sessions get conflict warnings the same two ways as Claude Code minus
mid-edit context injection: the hook submits repo snapshots at
sessionStart/stop so teammates see overlapping uncommitted edits, and agents
pull covibe_team operation: "state" -> repo_snapshot_conflicts plus
covibe_task operation: "check" file_conflicts before editing, as
instructed by the covibe-local agent-rules snippet, installed in AGENTS.md
and as a native always-on rule at .cursor/rules/covibe.mdc so the protocol
loads even on Cursor versions without AGENTS.md support. The reserved afterFileEdit hook is the
future slot for pushing per-edit overlap warnings into a running session.