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.
(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
- In Warp, run a Claude Code session with the plugin installed.
- Have another Claude Code session send it a message (Agent Teams
SendMessage, or any cross-session message — e.g. a background teammate reporting completion).
- Let the receiving session finish its turn so the
Stop hook fires while the teammate message is the most recent user-type transcript entry.
- 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.
- 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.
- 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.
- 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.
Summary
plugins/warp/scripts/on-stop.shderives the notification title ("the user's last prompt") by scraping the session transcript for the lasttype: "user"entry containing a text block. Claude Code's Agent Teams feature records incoming teammate messages astype: "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:(The mid-attribute cutoff is the script's 197-char slice; Warp's own title clamp compounds it.)
Environment
0.2026.07.01.09.21.01, macOS (Darwin 27)2.1.205warp@claude-code-warp2.1.0(installed via Warp's in-app prompt)Reproduction
SendMessage, or any cross-session message — e.g. a background teammate reporting completion).Stophook fires while the teammate message is the most recent user-type transcript entry.<teammate-message …>envelope as its title.Root cause
plugins/warp/scripts/on-stop.sh:39-51— the jq filter selects the lasttype == "user"entry that has string content or at least onetextblock, 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 ofteammate,system-reminder, or meta-flagged entries anywhere in the repo), so the envelope is captured verbatim asQUERY, blindly truncated at 197 chars +...(lines 60-62), and shipped to Warp, which rendersqueryas 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.
<teammate-messageat 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.summary="…"attribute as the title instead — it's the sender's own human-readable one-liner, exactly what a notification wants.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
on-stop.shis the one that reproduces this symptom, but the class of bug is latent wherever user-type entries are assumed human-authored.queryas a notification title verbatim; preferring the payload'ssummaryfield or applying word-aware truncation there would soften any future plugin-side regression. Happy to file that separately againstwarpdotdev/warpif useful.