Real-time tracing of Cursor AI agent conversations using Cursor Hooks.
Cursor hooks provide structured JSON via stdin for each event. We send spans immediately in real-time as events occur:
┌─────────────────────────────────────────────────────────────────┐
│ Cursor Agent │
├─────────────────────────────────────────────────────────────────┤
│ User Query → Thinking → Tool Calls → File Edits → Response │
│ ↓ ↓ ↓ ↓ ↓ │
│ [hooks fire with JSON input via stdin] │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │ │
↓ ↓ ↓ ↓ ↓
┌─────────┬─────────┬─────────────┬──────────┬──────────────┐
│ Store │ Send │ Send │ Send │ Send Root │
│ Prompt │ Thinking│ Shell/MCP/ │ File │ Span with │
│ │ Span │ File Spans │ Edit │ User I/O │
└─────────┴─────────┴─────────────┴──────────┴──────────────┘
│ │ │ │ │
└──────────┴───────────┴────────────┴────────────┘
↓
Respan Groups by
trace_unique_id on server
| Hook | JSON Input | Purpose |
|---|---|---|
beforeSubmitPrompt |
{ prompt, attachments } |
Store user input for root span |
afterAgentThought |
{ text, duration_ms } |
Send thinking span immediately |
afterShellExecution |
{ command, output, duration } |
Send shell command span immediately |
afterFileEdit |
{ file_path, edits } |
Send file edit span immediately |
afterMCPExecution |
{ tool_name, tool_input, result_json, duration } |
Send MCP tool span immediately |
afterAgentResponse |
{ text } |
Send root span with user input + agent output |
stop |
{ status, loop_count } |
Cleanup state file |
Common fields (all hooks): conversation_id, generation_id, model, cursor_version
Real-time Architecture: Each hook sends its span immediately. Respan groups spans with the same trace_unique_id on the server side into a hierarchical trace.
Bash/Zsh:
export RESPAN_API_KEY="your-api-key"
export TRACE_TO_RESPAN="true"
export CURSOR_RESPAN_DEBUG="true" # OptionalPowerShell:
$env:RESPAN_API_KEY = "your-api-key"
$env:TRACE_TO_RESPAN = "true"mkdir -p ~/.cursor/hooks
cp respan_hook.py ~/.cursor/hooks/Copy hooks.json.example to ~/.cursor/hooks.json:
{
"version": 1,
"hooks": {
"beforeSubmitPrompt": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"afterAgentThought": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"afterAgentResponse": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"afterShellExecution": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"afterFileEdit": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"afterMCPExecution": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
],
"stop": [
{ "command": "python ~/.cursor/hooks/respan_hook.py" }
]
}
}Restart to apply hooks.
Each agent response creates a trace with hierarchical spans:
Agent Response (root)
├── Thinking 1
├── Thinking 2
├── MCP: tool_name
├── Shell: command
├── Edit: filename
└── ...
IDs:
trace_unique_id={conversation_id}_{generation_id}(unique per turn)span_parent_id= root span ID (for children)thread_identifier=conversation_id(links all turns)
- Store User Input:
beforeSubmitPromptsaves user prompt to state file (needed for root span later) - Send Child Spans:
afterAgentThought,afterShellExecution,afterFileEdit,afterMCPExecutioneach send their span immediately - Send Root Span:
afterAgentResponsesends root span with user input + agent output - Cleanup: State cleared after root span is sent
- Fallback:
stophook cleans up any remaining state
Key Point: Spans are sent immediately as they occur, not batched. The Respan server groups them by trace_unique_id.
# Watch logs
tail -f ~/.cursor/state/respan_hook.log
# Check state
cat ~/.cursor/state/respan_state.json
# Clear state (reprocess)
rm ~/.cursor/state/respan_state.jsonPowerShell:
Get-Content "$env:USERPROFILE\.cursor\state\respan_hook.log" -Tail 50 -Wait| Issue | Solution |
|---|---|
| No logs | Check TRACE_TO_RESPAN=true is set and environment variables are loaded |
| API errors (403) | Verify RESPAN_API_KEY is valid and not expired |
| Only root span | Ensure all 7 hooks are configured in hooks.json |
| Missing user input in root span | Verify beforeSubmitPrompt hook is configured and running |
| Missing thinking | Ensure afterAgentThought hook is active |
| Spans not grouping | All spans must have same trace_unique_id format: {conversation_id}_{generation_id} |
See example_transcript.json for what Cursor sends to the hook via stdin for each event.
See example_trace_output.json for how the final trace appears in Respan after all spans are grouped.
| File | Purpose |
|---|---|
respan_hook.py |
Main hook script that processes events |
hooks.json.example |
Cursor hooks configuration template |
example_transcript.json |
Example of hook input data (what Cursor sends) |
example_trace_output.json |
Example of final trace structure (what appears in Respan) |
requirements.txt |
Python dependencies (requests) |
README.md |
Documentation |