Skip to content

fix(warp): skip synthetic transcript entries when picking the notification title - #78

Open
bobrnor wants to merge 1 commit into
warpdotdev:mainfrom
bobrnor:fix/stop-hook-skip-synthetic-transcript-entries
Open

fix(warp): skip synthetic transcript entries when picking the notification title#78
bobrnor wants to merge 1 commit into
warpdotdev:mainfrom
bobrnor:fix/stop-hook-skip-synthetic-transcript-entries

Conversation

@bobrnor

@bobrnor bobrnor commented Aug 11, 2026

Copy link
Copy Markdown

Problem

on-stop.sh (and, since #73, on-stop-failure.sh) titles the Warp notification with "the user's last prompt", scraped from the session transcript. The filter's only exclusion is tool-result entries — the comment at on-stop.sh:33-38 states the assumption that every other type: "user" entry is human-authored.

That assumption doesn't hold. Claude Code writes a number of its own injections into the transcript as user-type entries with ordinary string content, so they sail through the filter and become the notification title:

<bash-stdout>arn:aws:ecs:us-east-1:123456789012:task/prod-cluster/a4d9c84a37984f9d8f48b62f…
Task completed.

The most visible source is local ! bash mode — running !aws ecs list-tasks … in Claude Code records <bash-input>…</bash-input> and <bash-stdout>…</bash-stdout><bash-stderr></bash-stderr> as two plain-string user entries. If the turn ends after that, the ARN dump is the title.

Sampling 25 of my own recent transcripts, these user-type entries are all currently eligible to become a title:

entry count
<task-notification> 739
<local-command-stdout> 19
<command-name> 19
<system-reminder> 11
<bash-stdout> / <bash-input> 4 / 4

Root cause

on-stop.sh:39-51 and on-stop-failure.sh:28-40 (identical copies of the same jq filter) select the last type == "user" entry whose content is a plain string or an array containing a text block. Tool results are arrays of tool_result blocks and drop out; every synthetic injection with text content does not.

Fix

Move the transcript scraping into a sourceable scripts/extract-transcript.sh — which also removes the duplicated filter on-stop-failure.sh had to carry — and skip entries positively recognized as Claude Code internals, falling back to the previous real prompt.

Following the approach argued in #72:

  • Anchor on the ASCII envelope tags, not the surrounding English framing. <teammate-message> / <cross-session-message> arrive behind a "Another Claude session sent a message:" line that a localized build may translate, so those two are matched anywhere in the entry rather than as a prefix.
  • Only rewrite what is confidently recognized. No generic XML stripping and no rejection of odd-looking input — an unusual title beats a missing notification.
  • English-only markers ([Request interrupted by user…], Stop hook feedback:) are best-effort: a localized build falls through to today's behaviour rather than breaking.

When every candidate is filtered out, QUERY is empty and Warp falls back to its own "<agent> completed" title, which is still an improvement on raw XML.

legacy/on-stop.sh is untouched — it uses the first user entry, which is the human's opening prompt, so it never hit this.

Tests

18 cases added to tests/test-hooks.sh covering each synthetic form, plus the cases that guard against over-filtering: a human prompt that merely mentions a tag mid-sentence is kept, tool results are still skipped, text blocks are still joined, and degenerate transcripts (all-synthetic, no user entries, missing path) return empty rather than erroring.

=== Results: 74 passed, 0 failed ===

shellcheck -S warning is clean on all three touched scripts.

Verified end-to-end against the real transcript that produced the screenshot above — replaying it through the patched hook, query goes from the <bash-stdout>arn:aws:ecs:… dump to the user's actual prompt.

Relationship to existing issues and PRs

…ation title

The Stop and StopFailure hooks title Warp's notification with "the user's
last prompt", which they read from the session transcript. The filter only
excluded tool-result entries, on the assumption that every other `type:
"user"` entry is human-authored. Claude Code also writes its own injections
as user-type entries with ordinary text content, so those became the title:

    <bash-stdout>arn:aws:ecs:us-east-1:123:task/prod-cluster/a4d9c84a37…
    Task completed.

Local `!` bash output is the most visible case, but the same class covers
background subagent completions, slash-command wrappers, local command
output, system reminders and cross-session/teammate messages.

Move the transcript scraping into a sourceable extract-transcript.sh — which
also drops the copy of the filter that on-stop-failure.sh had to duplicate —
and skip entries that are confidently recognized as Claude Code internals,
falling back to the previous real prompt. Detection anchors on the ASCII
envelope tags rather than the surrounding English framing, which localized
builds may translate. Anything not positively recognized passes through
unchanged: an odd-looking title is better than a missing notification.

Verified against a real transcript that reproduced the screenshot above: the
title becomes the user's actual prompt instead of the ARN dump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Q7WdVu4eYxgDbEweugNDT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant