`subagentStop` never fires for background subagents; documented `summary` / `modified_files` / `agent_transcript_path` missing or null

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 no subagentStop arrived 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 subagentStop in 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_id being the parent’s Task tool_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: 0 and message_count: 0 despite real tool calls: raised in #166533 (#166533), logged as a separate accuracy issue. I also see loop_count: 0 and 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

  1. subagentStop fires for background subagents as well, with status reflecting 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”.
  2. summary and modified_files are populated as documented, or the docs are corrected to remove them.
  3. agent_transcript_path points at the subagent’s transcript file, which does exist on disk. Failing that, any field that maps subagent_id to 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_path is null when 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. So null here does not mean “transcripts disabled” as the docs state ? it means “nested”.
  • git_branch is documented as an optional subagentStart field, 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

Hey, thanks for the report. It’s one of the cleanest I’ve seen for hooks. The breakdown with the probe script, separated hypotheses, and raw payloads really speeds things up.

Confirmed on all three points:

  1. subagentStop doesn’t fire for background subagents. I can reproduce it. Foreground subagents emit start and stop, but background only emits start. This is a separate bug, I’ve reported it internally.

  2. summary and modified_files are missing from the payload. Confirmed too. The fields are documented, but not delivered. We’ll bundle this with item 1, and I’ll also flag it to the docs team. Either the fields get populated, or the docs need to be updated.

  3. agent_transcript_path: null plus subagent_id equals the Task tool call ID. This is the same root ID linkage issue you already found in #163054 and #166533. It’s being tracked. Thanks for cross-linking instead of reopening. Once the ID semantics are fixed, transcript resolving should follow.

I also noted your two smaller points, transcript_path being null for the nested spawner and git_branch missing, as docs notes.

Timeline-wise, I can’t share an ETA right now. Once there are updates on the fix, I’ll reply in the thread.