Skip to content

Stop hook notifies on every sub-agent completion (<task-notification> re-invocations), spamming a banner per sub-agent #67

@neeilya

Description

@neeilya

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions