Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
I instrumented subagentStart and subagentStop with a passive probe that dumps the raw stdin payload to a file, while building a hook-based session-recording tool. Three problems, in severity order.
1. subagentStop never fires for subagents started in the background
The docs describe the event unconditionally:
subagentStop ? Called when a subagent completes, errors, or is aborted. Can trigger follow-up actions.
Nothing there distinguishes foreground from background subagents. In practice only foreground subagents emit it. Background subagents emit subagentStart and then nothing, ever.
Three subagents observed across two sessions, all trivial explore tasks:
| Subagent | Mode | subagentStart |
subagentStop |
|---|---|---|---|
| A | foreground | 20:14:47 | 20:15:02 ? 15s later, correct |
| B | background | 20:15:14 | never ? watched for 90s after it finished |
| C | background | 20:19:08 | never ? watched for 110s after the parent turn ended |
The two obvious explanations are both ruled out:
- Not merely delayed until the subagent finishes. Subagent B’s own transcript file received its terminal
{"type": "turn_ended", "status": "success"}record at 20:15:26 ? the subagent was demonstrably done ? and nosubagentStoparrived in the next 90 seconds. - Not deferred until the result reaches the parent. For subagent C the parent turn was run to completion, the subagent’s result was delivered and rendered in the UI, and still no
subagentStopin the following 110 seconds.
2. summary and modified_files are absent from the subagentStop payload
Both are documented, and both appear in the docs’ example payload:
summary| string | Output summary from the subagent
modified_files| string | Files the subagent modified
Neither key exists in the payload actually received. Not null, not empty ? absent. The complete key set received is listed in the appendix below.
The nearest available field is description, which holds the short title the parent passed as the Task tool’s description argument. It is not a summary of the subagent’s output.
3. agent_transcript_path is null
agent_transcript_path| string | null | Path to the subagent’s own transcript file (separate from the parent conversation)
It is null, even though the subagent’s transcript does exist on disk at
~/.cursor/projects/<project>/agent-transcripts/<parent-conversation-id>/subagents/<subagent-uuid>.jsonl
and holds the complete message history. Transcripts are clearly enabled in this environment.
Why these three compound each other
Problem 3 is the documented escape hatch for problem 2 ? if the payload has no summary, a hook could at least read the transcript. But agent_transcript_path is null, and the only subagent identifier in the payload (subagent_id) is the Task tool-call ID (toolu_...), which does not appear anywhere in the transcript tree and cannot be mapped to the transcript’s UUID filename. I grepped the whole agent-transcripts tree for the toolu_... value and for the subagent UUID in the parent transcript; neither appears in the other’s context. So there is currently no supported way for a hook to find out what a subagent actually did.
Already reported ? cross-referencing rather than re-reporting
- The four ID fields collapsing into the session id, and
subagent_idbeing the parent’s Tasktool_call_id: #163054 (#163054) (confirmed by staff, filed internally) and #166533 (#166533). I reproduce both exactly on 3.12.30; nothing new to add. tool_call_count: 0andmessage_count: 0despite real tool calls: raised in #166533 (#166533), logged as a separate accuracy issue. I also seeloop_count: 0and can confirm the same behaviour.
Cross-refs (2 links): subagentStart hook: parent_conversation_id always equals conversation_id, and subagent conversations have no link back to their parent and subagentStart/subagentStop report 4 different id fields with the same session_id value, making it difficult to identify which prompt spawned a subagent
Steps to Reproduce
1. Save this probe script. It is deliberately passive ? it appends the raw payload to a file, explicitly allows on subagentStart, and prints nothing on subagentStop so that no followup_message is emitted:
#!/usr/bin/env python3
import json, sys, datetime
mode = sys.argv[1] if len(sys.argv) > 1 else ""
raw = sys.stdin.read()
try:
payload = json.loads(raw)
except Exception:
payload = {"_unparsed": raw}
with open("/tmp/subagent-probe.jsonl", "a") as fh:
fh.write(json.dumps({
"received_at": datetime.datetime.now().astimezone().isoformat(),
"probe_mode": mode,
"payload": payload,
}, ensure_ascii=False) + "\n")
if mode != "stop":
sys.stdout.write(json.dumps({"permission": "allow"}))
sys.exit(0)
2. Register it (picked up immediately, no restart needed):
{
"version": 1,
"hooks": {
"subagentStart": [
{ "command": "python3 /absolute/path/to/subagent-probe.py start", "timeout": 5 }
],
"subagentStop": [
{ "command": "python3 /absolute/path/to/subagent-probe.py stop", "timeout": 5 }
]
}
}
3. In an agent chat, have the agent spawn two subagents via the Task tool ? a trivial explore task such as “list the markdown files in the repository root”:
- once with
run_in_background: false - once with
run_in_background: true
4. Watch /tmp/subagent-probe.jsonl. The foreground subagent produces a subagentStart / subagentStop pair. The background one produces only subagentStart.
To confirm the background subagent genuinely finished, check its transcript at
~/.cursor/projects/<project>/agent-transcripts/<parent-id>/subagents/<subagent-uuid>.jsonl ?
the presence of a final {"type": "turn_ended", "status": "success"} record means the subagent is done.
5. For problems 2 and 3, inspect the single subagentStop payload from the foreground subagent and compare its keys against the documented table.
Expected Behavior
subagentStopfires for background subagents as well, withstatusreflecting the outcome. If firing is intentionally deferred, the docs should say when, and hook consumers need some way to distinguish “deferred” from “will never arrive”.summaryandmodified_filesare populated as documented, or the docs are corrected to remove them.agent_transcript_pathpoints at the subagent’s transcript file, which does exist on disk. Failing that, any field that mapssubagent_idto the transcript UUID would let consumers resolve it themselves.
Operating System
MacOS
Version Information
Cursor 3.12.30
Additional Information
Raw payloads below are sanitized: user_email, user paths and task content are replaced with placeholders. Field structure, types and ID relationships are verbatim.
Raw subagentStart payload (foreground subagent)
{
"hook_event_name": "subagentStart",
"conversation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"generation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"session_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"parent_conversation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"subagent_id": "toolu_01JfiMKCFdybtdceQud6UYEg",
"tool_call_id": "toolu_01JfiMKCFdybtdceQud6UYEg",
"subagent_type": "explore",
"subagent_model": "<model slug>",
"model": "<model slug>",
"is_parallel_worker": false,
"task": "<full prompt text handed to the subagent; verbatim and untruncated>",
"transcript_path": null,
"cursor_version": "3.12.30",
"user_email": "<redacted>@example.com",
"workspace_roots": ["/Users/<user>/<workspace>"]
}
Raw subagentStop payload (same subagent, 15 seconds later)
{
"hook_event_name": "subagentStop",
"conversation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"generation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"session_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"parent_conversation_id": "8b671fe0-954a-4f07-9189-ad3a94272a3e",
"subagent_id": "toolu_01JfiMKCFdybtdceQud6UYEg",
"subagent_type": "explore",
"status": "completed",
"description": "<short title the parent passed as the Task tool's description argument>",
"task": "<same full prompt text as in subagentStart>",
"duration_ms": 14764,
"message_count": 0,
"tool_call_count": 0,
"loop_count": 0,
"agent_transcript_path": null,
"transcript_path": null,
"model": "<model slug>",
"cursor_version": "3.12.30",
"user_email": "<redacted>@example.com",
"workspace_roots": ["/Users/<user>/<workspace>"]
}
Complete key set received (20 keys):
agent_transcript_path, conversation_id, cursor_version, description,
duration_ms, generation_id, hook_event_name, loop_count, message_count,
model, parent_conversation_id, session_id, status, subagent_id,
subagent_type, task, tool_call_count, transcript_path,
user_email, workspace_roots
Documented but never received: summary, modified_files.
Raw subagentStart payload (background subagent ? no subagentStop ever followed)
{
"hook_event_name": "subagentStart",
"conversation_id": "d41bb56f-b960-4316-a50c-b3c133a548c7",
"generation_id": "d41bb56f-b960-4316-a50c-b3c133a548c7",
"session_id": "d41bb56f-b960-4316-a50c-b3c133a548c7",
"parent_conversation_id": "d41bb56f-b960-4316-a50c-b3c133a548c7",
"subagent_id": "toolu_01LZPAGmhpry5St8mrqEqNUS",
"tool_call_id": "toolu_01LZPAGmhpry5St8mrqEqNUS",
"subagent_type": "explore",
"subagent_model": "<model slug>",
"model": "<model slug>",
"is_parallel_worker": false,
"task": "<full prompt text handed to the subagent>",
"transcript_path": "/Users/<user>/.cursor/projects/<project>/agent-transcripts/<id>/<id>.jsonl",
"cursor_version": "3.12.30",
"user_email": "<redacted>@example.com",
"workspace_roots": []
}
This subagent ran to completion and its result was delivered to the parent. The probe log contains exactly one line for it.
Watching the transcript is not a workable substitute. The obvious workaround for problem 1 is to watch the subagent’s transcript file for a turn_ended record instead of relying on the hook. Across 240 subagent transcripts on this machine (496 user-role messages), 173 of those turns have no turn_ended record before the next one ? the marker is reliably present on the final turn of a file (94.5%) but frequently missing on earlier ones. Some of those turns were probably superseded by a new prompt rather than completed normally, so I am not claiming a separate bug here; the practical point is only that a missing marker is ambiguous, so the transcript cannot substitute for the missing hook event.
Two smaller notes:
transcript_pathisnullwhen the agent spawning the subagent is itself a subagent, even though transcripts are enabled and the parent’s transcript exists on disk. When the spawning agent is a root session the field is populated correctly. Sonullhere does not mean “transcripts disabled” as the docs state ? it means “nested”.git_branchis documented as an optionalsubagentStartfield, but was absent even though the workspace root was a git repository on a normal branch.
Does this stop you from using Cursor
No - Cursor works, but with this issue