Summary
During long tasks that spawn sub-agents (Agent/Task tool, or background Bash), the Stop hook fires a desktop notification on every sub-agent completion, not just when the main agent finishes. On a multi-agent task this produces a banner per sub-agent while the main task is still running.
Root cause
When a background sub-agent/task completes, Claude Code injects a synthetic <task-notification> user message that re-invokes the main loop. That re-invocation is a short main-loop turn, so the Stop hook fires (on-stop.sh) and emits a warp://cli-agent stop notification.
It is genuinely the main loop's Stop — not SubagentStop (the plugin doesn't register it) and not PostToolUse (that sends a silent tool_complete status event). Confirmed directly from the rendered banner, whose title was literally:
'<task-notification>\n<task...' finished
Latest output: ...post-response background work via `waitUntil`, so there...
i.e. on-stop.sh picked the injected <task-notification> as the "last user prompt."
Why PR #59 doesn't fix it
#59 ("skip synthetic system messages when picking last user prompt") only changes which prompt is displayed — it relabels the banner with the real prompt instead of the raw tag. It does not stop the notification from firing, so the banner count is unchanged (and arguably more confusing, since each intermediate re-invocation now looks like the original task finished).
Proposed fix
Short-circuit on-stop.sh when the turn's last user message is a synthetic re-invocation (the <task-notification> marker). Real turn-ends still notify; the "done" signal after a long multi-agent task still arrives via the idle_prompt Notification hook.
# After TRANSCRIPT_PATH is resolved, before extracting QUERY:
LAST_USER=$(jq -rs '
[.[] | select(.type == "user")] | last |
if .message.content | type == "string" then .message.content
elif .message.content | type == "array" then
([.message.content[]? | select(.type == "text") | .text] | join(" "))
else "" end
' "$TRANSCRIPT_PATH" 2>/dev/null)
case "$LAST_USER" in
*'<task-notification>'*) exit 0 ;;
esac
This composes naturally with #59's synthetic-message detection — that PR already enumerates the synthetic tag set (task-notification, system-reminder, command-message, …); <task-notification> specifically should additionally suppress the notification, not just be skipped when choosing the title.
Environment
- Claude Code: 2.1.177 (also reproduced on 2.1.167)
- Warp: v0.2026.06.03.09.49.stable_01 (stable)
- Plugin: warp@claude-code-warp v2.0.0
- macOS
Verified the guard locally (all on-stop.sh copies incl. legacy/): sub-agent completions no longer notify, normal replies still do.
Summary
During long tasks that spawn sub-agents (Agent/Task tool, or background Bash), the
Stophook fires a desktop notification on every sub-agent completion, not just when the main agent finishes. On a multi-agent task this produces a banner per sub-agent while the main task is still running.Root cause
When a background sub-agent/task completes, Claude Code injects a synthetic
<task-notification>user message that re-invokes the main loop. That re-invocation is a short main-loop turn, so theStophook fires (on-stop.sh) and emits awarp://cli-agentstopnotification.It is genuinely the main loop's
Stop— notSubagentStop(the plugin doesn't register it) and notPostToolUse(that sends a silenttool_completestatus event). Confirmed directly from the rendered banner, whose title was literally:i.e.
on-stop.shpicked the injected<task-notification>as the "last user prompt."Why PR #59 doesn't fix it
#59 ("skip synthetic system messages when picking last user prompt") only changes which prompt is displayed — it relabels the banner with the real prompt instead of the raw tag. It does not stop the notification from firing, so the banner count is unchanged (and arguably more confusing, since each intermediate re-invocation now looks like the original task finished).
Proposed fix
Short-circuit
on-stop.shwhen the turn's lastusermessage is a synthetic re-invocation (the<task-notification>marker). Real turn-ends still notify; the "done" signal after a long multi-agent task still arrives via theidle_promptNotificationhook.This composes naturally with #59's synthetic-message detection — that PR already enumerates the synthetic tag set (
task-notification,system-reminder,command-message, …);<task-notification>specifically should additionally suppress the notification, not just be skipped when choosing the title.Environment
Verified the guard locally (all
on-stop.shcopies incl.legacy/): sub-agent completions no longer notify, normal replies still do.