Hook transcript: tool results indistinguishable from real user messages

Resolved 💬 4 comments Opened Feb 18, 2026 by jsnider3 Closed Mar 19, 2026

Problem

When writing hooks that need to measure how long Claude worked on a turn (e.g., to only play a notification sound for long-running tasks), the transcript JSONL makes it difficult to find the user's actual message.

Tool results are logged with type: "user" and userType: "external" — identical to real user-typed messages. The only way to distinguish them is checking for the presence of toolUseResult or sourceToolAssistantUUID fields:

// Real user message
{"type":"user","userType":"external","timestamp":"2026-02-18T06:45:16.545Z","message":{...}}

// Tool result (looks the same at first glance)
{"type":"user","userType":"external","timestamp":"2026-02-18T06:45:24.221Z","toolUseResult":{...},"sourceToolAssistantUUID":"..."}

Impact

A hook that measures elapsed time since the last user message needs to use a workaround like:

tac "$TRANSCRIPT" | jq -r 'select(.type=="user" and .userType=="external" and (has("toolUseResult") | not)) | .timestamp // empty' | head -1

This is fragile and non-obvious. Without knowing to check has("toolUseResult"), the naive filter select(.type=="user" and .userType=="external") picks up the most recent tool result instead of the user's message, making the elapsed time always appear as a few seconds.

Suggestion

Any of these would help:

  1. Use a distinct userType for tool results (e.g., "tool_result" instead of "external")
  2. Add a turn_start_timestamp field to the Stop/Notification hook event JSON, so hooks don't need to parse the transcript at all
  3. Add an elapsed_seconds or turn_duration field directly to the Stop hook event

Option 2 or 3 would be ideal — it would make duration-based hooks trivial without needing transcript parsing at all.

Use case

Playing an audible notification only when Claude has been working for 15+ seconds (so quick answers stay silent). This requires knowing when the user's turn started, which currently requires parsing the transcript and filtering out tool results.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗