Skip to content

Stop hook surfaces raw <teammate-message> XML as the notification title when an Agent Teams message is the last user-type transcript entry #72

Description

@fbartho

Summary

plugins/warp/scripts/on-stop.sh derives the notification title ("the user's last prompt") by scraping the session transcript for the last type: "user" entry containing a text block. Claude Code's Agent Teams feature records incoming teammate messages as type: "user" entries whose content is the raw internal delivery envelope — so when a teammate message is the most recent user-type entry at Stop time, Warp's in-app notification displays serialization internals instead of anything human-readable:

Another Claude session sent a message:
<teammate-message teammate_id="oss-record-researcher" color="…

Task completed.

screenshot of the above notification

(The mid-attribute cutoff is the script's 197-char slice; Warp's own title clamp compounds it.)

Environment

  • Warp Stable 0.2026.07.01.09.21.01, macOS (Darwin 27)
  • Claude Code 2.1.205
  • Plugin warp@claude-code-warp 2.1.0 (installed via Warp's in-app prompt)

Reproduction

  1. In Warp, run a Claude Code session with the plugin installed.
  2. Have another Claude Code session send it a message (Agent Teams SendMessage, or any cross-session message — e.g. a background teammate reporting completion).
  3. Let the receiving session finish its turn so the Stop hook fires while the teammate message is the most recent user-type transcript entry.
  4. The completion toast shows the raw <teammate-message …> envelope as its title.

Root cause

plugins/warp/scripts/on-stop.sh:39-51 — the jq filter selects the last type == "user" entry that has string content or at least one text block, on the assumption (per the comment at lines 33-38) that this distinguishes human prompts from tool-result messages. That assumption no longer holds: Claude Code injects teammate/cross-session messages into the transcript as user-type entries with plain-text content, beginning with a framing line followed by a <teammate-message teammate_id="…" color="…" summary="…">…</teammate-message> envelope. The filter has no provenance awareness (no handling of teammate, system-reminder, or meta-flagged entries anywhere in the repo), so the envelope is captured verbatim as QUERY, blindly truncated at 197 chars + ... (lines 60-62), and shipped to Warp, which renders query as the toast title.

Suggested fix (defense in depth — improve recognized patterns, pass everything else through)

The hook can't control what Claude Code writes into the transcript, so the goal is: accept garbage in, and only improve the output when the content is confidently recognized. Never reject or blank out unrecognized input — an odd-looking title is better than a missing notification.

  1. Skip confidently-recognized internal injections when selecting the "last user prompt", falling back to the previous qualifying entry. Anchor detection on the XML envelope tags (<teammate-message at the start of the text; content consisting solely of a <system-reminder>…</system-reminder> block) — these are stable ASCII identifiers that won't change under localization. The human-language framing line ("Another Claude session sent a message") may be translated in localized Claude Code builds, so treat it only as a secondary, best-effort signal, never the sole trigger. Matching must be Unicode-safe (message bodies can contain arbitrary Unicode; only the tag anchors are assumed ASCII). If Claude Code's transcript format exposes a meta/provenance flag (e.g. isMeta) on injected entries, prefer that over string matching entirely.
  2. When the last entry is a recognized teammate-message envelope and no earlier human prompt exists, extract its summary="…" attribute as the title instead — it's the sender's own human-readable one-liner, exactly what a notification wants.
  3. Anything not confidently recognized passes through unchanged. No generic XML stripping, no rejection of "weird" input — only rewrite patterns that are positively identified as Claude Code internals.

This is Claude-transcript-specific knowledge, so the fix belongs in this plugin's scripts only — no shared/abstracted change that would affect the codex/gemini/antigravity integrations (though those repos may want an equivalent audit if their CLIs grow multi-session messaging).

Related

  • The same last-user-entry scraping pattern appears in other scripts in this repo; on-stop.sh is the one that reproduces this symptom, but the class of bug is latent wherever user-type entries are assumed human-authored.
  • Defense-in-depth on the Warp side would also help: Warp core uses the plugin-supplied query as a notification title verbatim; preferring the payload's summary field or applying word-aware truncation there would soften any future plugin-side regression. Happy to file that separately against warpdotdev/warp if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions