Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
Summary
We consume Cursor’s agent hooks to attribute agent activity to the conversation that produced it. We’re seeing Cursor intermittently deliver tool-execution hook events in which the entire conversation-identity block is blank — conversation_id: "", session_id: "", generation_id: "", model: "", transcript_path: null — even though the machine/user envelope (cursor_version, workspace_roots, user_email) and the tool payload (tool_name, tool_input, tool_use_id) on the same event are fully populated.
Because the events carry no conversation id, any downstream consumer cannot attribute them to the conversation they belong to. We’ve mitigated this on our side, but the empty ids originate in the Cursor client, so we’re reporting it upstream. We were not able to reproduce it on demand; the strongest evidence is a static analysis of the shipped bundle plus a consistent fingerprint across a large volume of real events.
Environment
- Observed on Cursor desktop, notably
3.13.25,3.13.10, and3.9.16. - Other versions might be affected as well.
What we observe
-
The whole identity block blanks atomically. Whenever
conversation_idis empty,session_id,generation_id, andmodelare always empty too, andtranscript_pathis null — with zero exceptions in our data. It’s all-or-nothing, so the identity clearly comes from a single source that is either fully populated or fully empty. -
Only tool-execution hooks are affected. The blanks appear exclusively on:
preToolUse,postToolUse,postToolUseFailure,beforeReadFile,afterFileEdit,beforeShellExecution,afterShellExecution,beforeMCPExecution,afterMCPExecution.
They never appear on conversation-lifecycle hooks:
beforeSubmitPrompt,afterAgentResponse,afterAgentThought,preCompact,sessionStart,sessionEnd,subagentStart.
In practice this means a conversation’s prompts/responses/thoughts are correctly attributed while the tool calls inside that same conversation can be orphaned. -
The whole invocation is context-less, not a mid-flight loss. For a single tool invocation, the
before…andafter…hooks blank together (e.g.beforeShellExecutionandafterShellExecutionblank in equal counts). So the invocation is initiated without an id — it isn’t losing the id partway through. -
It’s tool-agnostic. The blank rate is roughly uniform across tool types (file reads, shell, search, writes all blank at a similar low baseline). Whatever tool happens to fire during a blank window is affected; it isn’t specific to one subsystem.
-
It’s intermittent and per-user-variable. Within a single user and a single event type, some events carry a real id and some don’t.
Strong correlation we found (reported as a lead, not a confirmed cause)
We found a very strong correlation with compaction happening mid-turn: in the affected cases, the empty ids begin immediately after a preCompact that fires during an agent turn, and then every subsequent tool call stays blank until the next user prompt, at which point ids are restored. Lifecycle events (thoughts/responses) emitted during that same window keep the correct id — only the tool events blank.
We want to be explicit that we are not claiming preCompact/compaction is the root cause:
- We could not reproduce the blanking on demand, including by deliberately triggering compaction many times (in normal usage and in a dedicated harness). Compaction is harmless for the vast majority of users/sessions in our data.
- So compaction may be correlated with, rather than causing, the underlying context loss — e.g. it may just be the most common event that coincides with the gated path being active, or it may perturb some shared state that is the real culprit. There may also be additional, unrelated triggers.
We’re sharing it because the correlation is high and may help you localize the state that gets dropped.
What we ruled out
- Our own ingestion / plugin. Our hook dispatcher is a verbatim pass-through — it reads the hook body from stdin and forwards it byte-for-byte without parsing or rewriting any field. The empty strings are present in exactly what Cursor writes to the hook’s stdin.
- Privacy/Ghost mode, subagents, model provider / enterprise (Bedrock) config, cross-tool Claude Code hook loading, and restart/resume — each independently checked and excluded (e.g. subagent lifecycle events never blank, and users with zero subagent activity still blank tool calls; blanks span multiple providers; prompts never blank, which rules out session-wide resume/privacy explanations).
Code-level findings (decompiled 3.13.25 bundle)
This is likely the most useful part for you. Static inspection of the shipped renderer bundle (out/vs/workbench/workbench.desktop.main.js) and the agent-exec extension bundle (extensions/cursor-agent-exec/dist/main.js) shows two different identity sources for the two hook families, and a hard-coded empty fallback on the tool-execution side:
-
Lifecycle hooks read the id straight off the live composer, e.g.:
executeHookForStep(dh.afterAgentThought, { conversation_id: this.composerDataHandle.data.composerId, ... })These are the hooks that never blank.
-
Tool-execution hooks build their payload from a shared exec/resource context that defaults to empty string when the slot isn’t populated, e.g.:
executeHookForStep(dh.beforeReadFile, { conversation_id: m ?? "", generation_id: v ?? "", model: b ?? "", ... }) // agent-exec base hook extractor: conversation_id: <ctx>.conversationId ?? "" -
There is a hard-coded empty-identity fallback used when the stream-metadata context slot is unset (in the exec-instance factory):
{ conversationId: "", composerId: "", requestId: "", modelName: "" }This exactly matches the atomic all-empty signature we see in production.
-
Symbol drift on the context key. The bundle contains both a registry symbol
Symbol.for("cursor.agentStreamMetadata")(×1) and plain, module-localSymbol("agentStreamMetadata")(×2). A writer keying the context off one symbol and a reader looking it up under a different same-named symbol would miss and hit the empty fallback above — a plausible mechanism for an intermittent, context-dependent blank. -
A bare-context summarization path (
summarize(Kg())) is present, which is consistent with (though not proof of) the compaction correlation above: a turn that continues after a mid-turn summarization may run on a context that was never re-seeded with the stream metadata.
Our working hypothesis, stated as a hypothesis: the tool-execution hook path reads conversation identity from a context/metadata slot that is occasionally unset for a gated cohort, and when unset it silently falls back to empty strings instead of the live conversation id. Compaction mid-turn appears to be a strong correlate of entering that state, but we haven’t proven causation.
Steps to Reproduce
We built a local harness that captures every hook’s raw payload and drove Cursor 3.13.25 through: foreground agent with sandbox on and off, Cmd-K inline edits, compaction/summarize (fired repeatedly), interrupt→resume, parallel tool bursts, background shells, and long-running MCP calls. Across ~400 tool events we captured 0 blanks, which suggests the buggy path is enabled for a cohort our machine isn’t in — reinforcing the feature-gate/rollout reading.
Operating System
MacOS
Version Information
- Observed on Cursor desktop, notably
3.13.25,3.13.10, and3.9.16. - Other versions might be affected as well.
Does this stop you from using Cursor
No - Cursor works, but with this issue