From 067b9771ce1dbd31ba5018da095e72431a6ace59 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 19 Mar 2026 09:48:50 -0400 Subject: [PATCH 01/16] extend warp claude plugin to include more detailed hooks and structured responses --- plugins/warp/hooks/hooks.json | 12 ++- plugins/warp/scripts/on-notification.sh | 25 +++++- plugins/warp/scripts/on-permission-request.sh | 45 +++++++++++ plugins/warp/scripts/on-stop.sh | 77 ++++++++++++------- plugins/warp/scripts/warp-notify.sh | 3 + 5 files changed, 129 insertions(+), 33 deletions(-) create mode 100755 plugins/warp/scripts/on-permission-request.sh diff --git a/plugins/warp/hooks/hooks.json b/plugins/warp/hooks/hooks.json index 6e44d07..39f1b13 100644 --- a/plugins/warp/hooks/hooks.json +++ b/plugins/warp/hooks/hooks.json @@ -24,7 +24,7 @@ ], "Notification": [ { - "matcher": "*", + "matcher": "idle_prompt", "hooks": [ { "type": "command", @@ -32,6 +32,16 @@ } ] } + ], + "PermissionRequest": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-permission-request.sh" + } + ] + } ] } } diff --git a/plugins/warp/scripts/on-notification.sh b/plugins/warp/scripts/on-notification.sh index 378923e..bcc26d7 100755 --- a/plugins/warp/scripts/on-notification.sh +++ b/plugins/warp/scripts/on-notification.sh @@ -1,14 +1,31 @@ #!/bin/bash -# Hook script for Claude Code Notification event -# Sends a Warp notification when Claude needs user input +# Hook script for Claude Code Notification event (idle_prompt only) +# Sends a structured Warp notification when Claude has been idle SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Read hook input from stdin INPUT=$(cat) -# Extract the notification message +# Extract metadata from the hook input +NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null) +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null) [ -z "$MSG" ] && MSG="Input needed" +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) +PROJECT="" +if [ -n "$CWD" ]; then + PROJECT=$(basename "$CWD") +fi -"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG" +# Build structured JSON payload +BODY=$(jq -nc \ + --arg agent "claude" \ + --arg event "$NOTIF_TYPE" \ + --arg session_id "$SESSION_ID" \ + --arg cwd "$CWD" \ + --arg project "$PROJECT" \ + --arg summary "$MSG" \ + '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, summary:$summary}') + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh new file mode 100755 index 0000000..60f238e --- /dev/null +++ b/plugins/warp/scripts/on-permission-request.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Hook script for Claude Code PermissionRequest event +# Sends a structured Warp notification when Claude needs permission to run a tool + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Read hook input from stdin +INPUT=$(cat) + +# Extract metadata from the hook input +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null) +TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) +# Fallback to empty object if jq failed or returned empty +[ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}' +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) +PROJECT="" +if [ -n "$CWD" ]; then + PROJECT=$(basename "$CWD") +fi + +# Build a human-readable summary for the notification body +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '.tool_input | if .command then .command elif .file_path then .file_path else (tostring | .[0:80]) end // ""' 2>/dev/null) +SUMMARY="Wants to run $TOOL_NAME" +if [ -n "$TOOL_PREVIEW" ]; then + if [ ${#TOOL_PREVIEW} -gt 120 ]; then + TOOL_PREVIEW="${TOOL_PREVIEW:0:117}..." + fi + SUMMARY="$SUMMARY: $TOOL_PREVIEW" +fi + +# Build structured JSON payload +# tool_input is passed as raw JSON (not a string) so Warp can inspect it directly +BODY=$(jq -nc \ + --arg agent "claude" \ + --arg event "permission_request" \ + --arg session_id "$SESSION_ID" \ + --arg cwd "$CWD" \ + --arg project "$PROJECT" \ + --arg summary "$SUMMARY" \ + --arg tool_name "$TOOL_NAME" \ + --argjson tool_input "$TOOL_INPUT" \ + '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, summary:$summary, tool_name:$tool_name, tool_input:$tool_input}') + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index 2a45dd9..f959ea7 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -1,48 +1,69 @@ #!/bin/bash # Hook script for Claude Code Stop event -# Sends a Warp notification when Claude completes a task +# Sends a structured Warp notification when Claude completes a task SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Read hook input from stdin INPUT=$(cat) -# Extract transcript path from the hook input -TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) +# Skip if a stop hook is already active (prevents double-notification) +STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null) +if [ "$STOP_HOOK_ACTIVE" = "true" ]; then + exit 0 +fi -# Default message -MSG="Task completed" +# Extract metadata from the hook input +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) +PROJECT="" +if [ -n "$CWD" ]; then + PROJECT=$(basename "$CWD") +fi -# Try to extract prompt and response from the transcript (JSONL format) +# Extract the last user prompt and assistant response from the transcript. +# Small delay to allow Claude Code to flush the current turn to the transcript file. +# The Stop hook fires before the transcript is fully written. +sleep 0.3 +QUERY="" +RESPONSE="" if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then - # Get the first user prompt - PROMPT=$(jq -rs ' - [.[] | select(.type == "user")] | first | .message.content // empty + # Get the last user prompt (most recent turn, not the first in the session) + # .message.content can be a string or an array of {type, text} objects + QUERY=$(jq -rs ' + [.[] | select(.type == "user")] | last | + if .message.content | type == "array" + then [.message.content[] | select(.type == "text") | .text] | join(" ") + else .message.content // empty + end ' "$TRANSCRIPT_PATH" 2>/dev/null) - + # Get the last assistant response RESPONSE=$(jq -rs ' [.[] | select(.type == "assistant" and .message.content)] | last | [.message.content[] | select(.type == "text") | .text] | join(" ") ' "$TRANSCRIPT_PATH" 2>/dev/null) - - if [ -n "$PROMPT" ] && [ -n "$RESPONSE" ]; then - # Truncate prompt to 50 chars - if [ ${#PROMPT} -gt 50 ]; then - PROMPT="${PROMPT:0:47}..." - fi - # Truncate response to 120 chars - if [ ${#RESPONSE} -gt 120 ]; then - RESPONSE="${RESPONSE:0:117}..." - fi - MSG="\"${PROMPT}\" → ${RESPONSE}" - elif [ -n "$RESPONSE" ]; then - # Fallback to just response if no prompt found - if [ ${#RESPONSE} -gt 175 ]; then - RESPONSE="${RESPONSE:0:172}..." - fi - MSG="$RESPONSE" + + # Truncate for notification display + if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then + QUERY="${QUERY:0:197}..." + fi + if [ -n "$RESPONSE" ] && [ ${#RESPONSE} -gt 200 ]; then + RESPONSE="${RESPONSE:0:197}..." fi fi -"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG" +# Build structured JSON payload +BODY=$(jq -nc \ + --arg agent "claude" \ + --arg event "stop" \ + --arg session_id "$SESSION_ID" \ + --arg cwd "$CWD" \ + --arg project "$PROJECT" \ + --arg query "$QUERY" \ + --arg response "$RESPONSE" \ + --arg transcript_path "$TRANSCRIPT_PATH" \ + '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, query:$query, response:$response, transcript_path:$transcript_path}') + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/warp-notify.sh b/plugins/warp/scripts/warp-notify.sh index 6ca0588..77a7855 100755 --- a/plugins/warp/scripts/warp-notify.sh +++ b/plugins/warp/scripts/warp-notify.sh @@ -1,6 +1,9 @@ #!/bin/bash # Warp notification utility using OSC escape sequences # Usage: warp-notify.sh <body> +# +# For structured Warp notifications, title should be "warp://cli-agent" +# and body should be a JSON string matching the cli-agent notification schema. TITLE="${1:-Notification}" BODY="${2:-}" From daf3622405983a13ea424b5614bebbab069a9846 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 09:54:17 -0400 Subject: [PATCH 02/16] add test file for hooks --- plugins/warp/tests/test-hooks.sh | 122 +++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100755 plugins/warp/tests/test-hooks.sh diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh new file mode 100755 index 0000000..264e692 --- /dev/null +++ b/plugins/warp/tests/test-hooks.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# Tests for the Warp Claude Code plugin hook scripts. +# +# Validates that each hook script produces correctly structured JSON payloads +# by piping mock Claude Code hook input into the scripts and checking the output. +# +# Usage: ./tests/test-hooks.sh +# +# Since the hook scripts write OSC sequences to /dev/tty (not stdout), +# we test build-payload.sh directly — it's the shared JSON construction logic +# that all hook scripts use. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" + +PASSED=0 +FAILED=0 + +# --- Test helpers --- + +assert_eq() { + local test_name="$1" + local expected="$2" + local actual="$3" + if [ "$expected" = "$actual" ]; then + echo " ✓ $test_name" + PASSED=$((PASSED + 1)) + else + echo " ✗ $test_name" + echo " expected: $expected" + echo " actual: $actual" + FAILED=$((FAILED + 1)) + fi +} + +assert_json_field() { + local test_name="$1" + local json="$2" + local field="$3" + local expected="$4" + local actual + actual=$(echo "$json" | jq -r "$field" 2>/dev/null) + assert_eq "$test_name" "$expected" "$actual" +} + +# --- Tests --- + +echo "=== build-payload.sh ===" + +echo "" +echo "--- Common fields ---" +PAYLOAD=$(build_payload '{"session_id":"sess-123","cwd":"/Users/alice/my-project"}' "stop") +assert_json_field "v is 1" "$PAYLOAD" ".v" "1" +assert_json_field "agent is claude" "$PAYLOAD" ".agent" "claude" +assert_json_field "event is stop" "$PAYLOAD" ".event" "stop" +assert_json_field "session_id extracted" "$PAYLOAD" ".session_id" "sess-123" +assert_json_field "cwd extracted" "$PAYLOAD" ".cwd" "/Users/alice/my-project" +assert_json_field "project is basename of cwd" "$PAYLOAD" ".project" "my-project" + +echo "" +echo "--- Common fields with missing data ---" +PAYLOAD=$(build_payload '{}' "stop") +assert_json_field "empty session_id" "$PAYLOAD" ".session_id" "" +assert_json_field "empty cwd" "$PAYLOAD" ".cwd" "" +assert_json_field "empty project" "$PAYLOAD" ".project" "" + +echo "" +echo "--- Extra args are merged ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "stop" \ + --arg query "hello" \ + --arg response "world") +assert_json_field "query merged" "$PAYLOAD" ".query" "hello" +assert_json_field "response merged" "$PAYLOAD" ".response" "world" +assert_json_field "common fields still present" "$PAYLOAD" ".session_id" "s1" + +echo "" +echo "--- Stop event ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "stop" \ + --arg query "write a haiku" \ + --arg response "Memory is safe, the borrow checker stands guard" \ + --arg transcript_path "/tmp/transcript.jsonl") +assert_json_field "event is stop" "$PAYLOAD" ".event" "stop" +assert_json_field "query present" "$PAYLOAD" ".query" "write a haiku" +assert_json_field "response present" "$PAYLOAD" ".response" "Memory is safe, the borrow checker stands guard" +assert_json_field "transcript_path present" "$PAYLOAD" ".transcript_path" "/tmp/transcript.jsonl" + +echo "" +echo "--- Permission request event ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "permission_request" \ + --arg summary "Wants to run Bash: rm -rf /tmp" \ + --arg tool_name "Bash" \ + --argjson tool_input '{"command":"rm -rf /tmp"}') +assert_json_field "event is permission_request" "$PAYLOAD" ".event" "permission_request" +assert_json_field "summary present" "$PAYLOAD" ".summary" "Wants to run Bash: rm -rf /tmp" +assert_json_field "tool_name present" "$PAYLOAD" ".tool_name" "Bash" +assert_json_field "tool_input.command present" "$PAYLOAD" ".tool_input.command" "rm -rf /tmp" + +echo "" +echo "--- Idle prompt event ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj","notification_type":"idle_prompt"}' "idle_prompt" \ + --arg summary "Claude is waiting for your input") +assert_json_field "event is idle_prompt" "$PAYLOAD" ".event" "idle_prompt" +assert_json_field "summary present" "$PAYLOAD" ".summary" "Claude is waiting for your input" + +echo "" +echo "--- JSON special characters in values ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "stop" \ + --arg query 'what does "hello world" mean?' \ + --arg response 'It means greeting. Use: printf("hello")') +assert_json_field "quotes in query preserved" "$PAYLOAD" ".query" 'what does "hello world" mean?' +assert_json_field "parens in response preserved" "$PAYLOAD" ".response" 'It means greeting. Use: printf("hello")' + +# --- Summary --- + +echo "" +echo "=== Results: $PASSED passed, $FAILED failed ===" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi From 0e9636f2eae381b32fa62bc2aedff9771dec5c5c Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 09:54:25 -0400 Subject: [PATCH 03/16] centralize hook structure --- plugins/warp/scripts/build-payload.sh | 40 +++++++++++++++++++ plugins/warp/scripts/on-notification.sh | 20 ++-------- plugins/warp/scripts/on-permission-request.sh | 23 +++-------- plugins/warp/scripts/on-stop.sh | 22 ++-------- 4 files changed, 53 insertions(+), 52 deletions(-) create mode 100644 plugins/warp/scripts/build-payload.sh diff --git a/plugins/warp/scripts/build-payload.sh b/plugins/warp/scripts/build-payload.sh new file mode 100644 index 0000000..4c90970 --- /dev/null +++ b/plugins/warp/scripts/build-payload.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Builds a structured JSON notification payload for warp://cli-agent. +# +# Usage: source this file, then call build_payload with event-specific fields. +# +# Example: +# source "$(dirname "${BASH_SOURCE[0]}")/build-payload.sh" +# BODY=$(build_payload "$INPUT" "stop" \ +# --arg query "$QUERY" \ +# --arg response "$RESPONSE" \ +# --arg transcript_path "$TRANSCRIPT_PATH") +# +# The function extracts common fields (session_id, cwd, project) from the +# hook's stdin JSON (passed as $1), then merges any extra jq args you pass. + +build_payload() { + local input="$1" + local event="$2" + shift 2 + + # Extract common fields from the hook input + local session_id cwd project + session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) + cwd=$(echo "$input" | jq -r '.cwd // empty' 2>/dev/null) + project="" + if [ -n "$cwd" ]; then + project=$(basename "$cwd") + fi + + # Build the payload: common fields + any extra args passed by the caller. + # Extra args should be jq flag pairs like: --arg key "value" or --argjson key '{"a":1}' + jq -nc \ + --arg agent "claude" \ + --arg event "$event" \ + --arg session_id "$session_id" \ + --arg cwd "$cwd" \ + --arg project "$project" \ + "$@" \ + '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project} + $ARGS.named' +} diff --git a/plugins/warp/scripts/on-notification.sh b/plugins/warp/scripts/on-notification.sh index bcc26d7..0420d1d 100755 --- a/plugins/warp/scripts/on-notification.sh +++ b/plugins/warp/scripts/on-notification.sh @@ -3,29 +3,17 @@ # Sends a structured Warp notification when Claude has been idle SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin INPUT=$(cat) -# Extract metadata from the hook input +# Extract notification-specific fields NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null) -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null) [ -z "$MSG" ] && MSG="Input needed" -CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) -PROJECT="" -if [ -n "$CWD" ]; then - PROJECT=$(basename "$CWD") -fi -# Build structured JSON payload -BODY=$(jq -nc \ - --arg agent "claude" \ - --arg event "$NOTIF_TYPE" \ - --arg session_id "$SESSION_ID" \ - --arg cwd "$CWD" \ - --arg project "$PROJECT" \ - --arg summary "$MSG" \ - '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, summary:$summary}') +BODY=$(build_payload "$INPUT" "$NOTIF_TYPE" \ + --arg summary "$MSG") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh index 60f238e..799e708 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -3,23 +3,18 @@ # Sends a structured Warp notification when Claude needs permission to run a tool SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin INPUT=$(cat) -# Extract metadata from the hook input -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +# Extract permission-request-specific fields TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null) TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) # Fallback to empty object if jq failed or returned empty [ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}' -CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) -PROJECT="" -if [ -n "$CWD" ]; then - PROJECT=$(basename "$CWD") -fi -# Build a human-readable summary for the notification body +# Build a human-readable summary TOOL_PREVIEW=$(echo "$INPUT" | jq -r '.tool_input | if .command then .command elif .file_path then .file_path else (tostring | .[0:80]) end // ""' 2>/dev/null) SUMMARY="Wants to run $TOOL_NAME" if [ -n "$TOOL_PREVIEW" ]; then @@ -29,17 +24,9 @@ if [ -n "$TOOL_PREVIEW" ]; then SUMMARY="$SUMMARY: $TOOL_PREVIEW" fi -# Build structured JSON payload -# tool_input is passed as raw JSON (not a string) so Warp can inspect it directly -BODY=$(jq -nc \ - --arg agent "claude" \ - --arg event "permission_request" \ - --arg session_id "$SESSION_ID" \ - --arg cwd "$CWD" \ - --arg project "$PROJECT" \ +BODY=$(build_payload "$INPUT" "permission_request" \ --arg summary "$SUMMARY" \ --arg tool_name "$TOOL_NAME" \ - --argjson tool_input "$TOOL_INPUT" \ - '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, summary:$summary, tool_name:$tool_name, tool_input:$tool_input}') + --argjson tool_input "$TOOL_INPUT") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index f959ea7..e2122dd 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -3,6 +3,7 @@ # Sends a structured Warp notification when Claude completes a task SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin INPUT=$(cat) @@ -13,18 +14,10 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then exit 0 fi -# Extract metadata from the hook input -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) -CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) -PROJECT="" -if [ -n "$CWD" ]; then - PROJECT=$(basename "$CWD") -fi - # Extract the last user prompt and assistant response from the transcript. # Small delay to allow Claude Code to flush the current turn to the transcript file. # The Stop hook fires before the transcript is fully written. +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) sleep 0.3 QUERY="" RESPONSE="" @@ -54,16 +47,9 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then fi fi -# Build structured JSON payload -BODY=$(jq -nc \ - --arg agent "claude" \ - --arg event "stop" \ - --arg session_id "$SESSION_ID" \ - --arg cwd "$CWD" \ - --arg project "$PROJECT" \ +BODY=$(build_payload "$INPUT" "stop" \ --arg query "$QUERY" \ --arg response "$RESPONSE" \ - --arg transcript_path "$TRANSCRIPT_PATH" \ - '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project, query:$query, response:$response, transcript_path:$transcript_path}') + --arg transcript_path "$TRANSCRIPT_PATH") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" From ce6db784532f63b60afadb3956451aed69fa1a11 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 10:06:50 -0400 Subject: [PATCH 04/16] add warp terminal guard and start notificatoin --- plugins/warp/scripts/on-session-start.sh | 26 ++++++++++++++++++------ plugins/warp/scripts/warp-notify.sh | 7 +++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index 4d5e7fb..4d5b605 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -1,20 +1,34 @@ #!/bin/bash # Hook script for Claude Code SessionStart event -# Shows welcome message and Warp detection status +# Shows welcome message, Warp detection status, and emits plugin version -# Check if running in Warp terminal +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" + +# Read hook input from stdin +INPUT=$(cat) + +# Read plugin version from plugin.json +PLUGIN_VERSION=$(jq -r '.version // "unknown"' "$SCRIPT_DIR/../.claude-plugin/plugin.json" 2>/dev/null) + +# Emit structured notification with plugin version so Warp can track it +BODY=$(build_payload "$INPUT" "session_start" \ + --arg plugin_version "$PLUGIN_VERSION") +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" + +# Output system message for the Claude Code UI if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then # Running in Warp - notifications will work - cat << 'EOF' + cat << EOF { - "systemMessage": "🔔 Warp plugin active. You'll receive native Warp notifications when tasks complete or input is needed." + "systemMessage": "🔔 Warp plugin v${PLUGIN_VERSION} active. You'll receive native Warp notifications when tasks complete or input is needed." } EOF else # Not running in Warp - suggest installing - cat << 'EOF' + cat << EOF { - "systemMessage": "â„šī¸ Warp plugin installed but you're not running in Warp terminal. Install Warp (https://warp.dev) to get native notifications when Claude completes tasks or needs input." + "systemMessage": "â„šī¸ Warp plugin v${PLUGIN_VERSION} installed but you're not running in Warp terminal. Install Warp (https://warp.dev) to get native notifications when Claude completes tasks or needs input." } EOF fi diff --git a/plugins/warp/scripts/warp-notify.sh b/plugins/warp/scripts/warp-notify.sh index 77a7855..b13a71c 100755 --- a/plugins/warp/scripts/warp-notify.sh +++ b/plugins/warp/scripts/warp-notify.sh @@ -5,6 +5,13 @@ # For structured Warp notifications, title should be "warp://cli-agent" # and body should be a JSON string matching the cli-agent notification schema. +# Only emit notifications when running in Warp. +# Otherwise, folks that use warp _and_ another terminal will get +# garbled notifications whenever they run claude elsewhere. +if [ "$TERM_PROGRAM" != "WarpTerminal" ]; then + exit 0 +fi + TITLE="${1:-Notification}" BODY="${2:-}" From 84e8e1acd96b6d927a53da6d3360c223f7028469 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 11:58:22 -0400 Subject: [PATCH 05/16] add prompt submit and post-tool-use hooks --- plugins/warp/hooks/hooks.json | 20 ++++++++++++++++++++ plugins/warp/scripts/on-post-tool-use.sh | 17 +++++++++++++++++ plugins/warp/scripts/on-prompt-submit.sh | 21 +++++++++++++++++++++ plugins/warp/scripts/on-session-start.sh | 17 ----------------- plugins/warp/scripts/on-stop.sh | 16 +++++++++++++--- 5 files changed, 71 insertions(+), 20 deletions(-) create mode 100755 plugins/warp/scripts/on-post-tool-use.sh create mode 100755 plugins/warp/scripts/on-prompt-submit.sh diff --git a/plugins/warp/hooks/hooks.json b/plugins/warp/hooks/hooks.json index 39f1b13..f2597ee 100644 --- a/plugins/warp/hooks/hooks.json +++ b/plugins/warp/hooks/hooks.json @@ -42,6 +42,26 @@ } ] } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-prompt-submit.sh" + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-tool-use.sh" + } + ] + } ] } } diff --git a/plugins/warp/scripts/on-post-tool-use.sh b/plugins/warp/scripts/on-post-tool-use.sh new file mode 100755 index 0000000..574f56a --- /dev/null +++ b/plugins/warp/scripts/on-post-tool-use.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Hook script for Claude Code PostToolUse event +# Sends a structured Warp notification after a tool call completes, +# transitioning the session status from Blocked back to Running. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" + +# Read hook input from stdin +INPUT=$(cat) + +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "tool_complete" \ + --arg tool_name "$TOOL_NAME") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-prompt-submit.sh b/plugins/warp/scripts/on-prompt-submit.sh new file mode 100755 index 0000000..150997e --- /dev/null +++ b/plugins/warp/scripts/on-prompt-submit.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Hook script for Claude Code UserPromptSubmit event +# Sends a structured Warp notification when the user submits a prompt, +# transitioning the session status from idle/blocked back to running. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/build-payload.sh" + +# Read hook input from stdin +INPUT=$(cat) + +# Extract the user's prompt +QUERY=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null) +if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then + QUERY="${QUERY:0:197}..." +fi + +BODY=$(build_payload "$INPUT" "prompt_submit" \ + --arg query "$QUERY") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index 4d5b605..8b28019 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -15,20 +15,3 @@ PLUGIN_VERSION=$(jq -r '.version // "unknown"' "$SCRIPT_DIR/../.claude-plugin/pl BODY=$(build_payload "$INPUT" "session_start" \ --arg plugin_version "$PLUGIN_VERSION") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" - -# Output system message for the Claude Code UI -if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then - # Running in Warp - notifications will work - cat << EOF -{ - "systemMessage": "🔔 Warp plugin v${PLUGIN_VERSION} active. You'll receive native Warp notifications when tasks complete or input is needed." -} -EOF -else - # Not running in Warp - suggest installing - cat << EOF -{ - "systemMessage": "â„šī¸ Warp plugin v${PLUGIN_VERSION} installed but you're not running in Warp terminal. Install Warp (https://warp.dev) to get native notifications when Claude completes tasks or needs input." -} -EOF -fi diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index e2122dd..62e0132 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -22,10 +22,20 @@ sleep 0.3 QUERY="" RESPONSE="" if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then - # Get the last user prompt (most recent turn, not the first in the session) - # .message.content can be a string or an array of {type, text} objects + # Get the last human prompt from the transcript. + # "user" type messages include both human prompts and tool-result messages. + # Human prompts have content that is either a plain string or an array + # containing {type:"text"} blocks. Tool-result messages have content arrays + # containing only {type:"tool_result"} blocks. We filter to messages that + # have at least one "text" block (or are a plain string). QUERY=$(jq -rs ' - [.[] | select(.type == "user")] | last | + [ + .[] | select(.type == "user") | + if .message.content | type == "string" then . + elif [.message.content[] | select(.type == "text")] | length > 0 then . + else empty + end + ] | last | if .message.content | type == "array" then [.message.content[] | select(.type == "text") | .text] | join(" ") else .message.content // empty From 616bb7039a59ddfe3963dfe79accc4a622d69d12 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 15:44:55 -0400 Subject: [PATCH 06/16] fix if then else --- plugins/warp/scripts/on-permission-request.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh index 799e708..938cc8a 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -15,7 +15,7 @@ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) [ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}' # Build a human-readable summary -TOOL_PREVIEW=$(echo "$INPUT" | jq -r '.tool_input | if .command then .command elif .file_path then .file_path else (tostring | .[0:80]) end // ""' 2>/dev/null) +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path else (tostring | .[0:80]) end) // ""' 2>/dev/null) SUMMARY="Wants to run $TOOL_NAME" if [ -n "$TOOL_PREVIEW" ]; then if [ ${#TOOL_PREVIEW} -gt 120 ]; then From 9b1f30838cfdc15b41663e5292023f6bd1a41eff Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Thu, 19 Mar 2026 16:48:22 -0400 Subject: [PATCH 07/16] add test action --- .github/workflows/test.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dd34d55 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Plugin Tests +on: + pull_request: + push: + branches: [main] + +jobs: + plugin-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + # Auto-discovers and runs all test-*.sh scripts under any tests/ directory. + # To add a new test, just drop a test-*.sh file in a tests/ folder. + run: | + shopt -s globstar nullglob + failed=0 + for f in **/tests/test-*.sh; do + echo "--- $f ---" + bash "$f" || failed=1 + done + exit $failed From 304b9b805f4ee5856f313a4438b0f75e3e792815 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Fri, 20 Mar 2026 12:12:59 -0400 Subject: [PATCH 08/16] show message when jq is not installed --- plugins/warp/scripts/on-session-start.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index 8b28019..b4631a1 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -2,6 +2,15 @@ # Hook script for Claude Code SessionStart event # Shows welcome message, Warp detection status, and emits plugin version +if ! command -v jq &>/dev/null; then + cat << 'EOF' +{ + "systemMessage": "âš ī¸ Warp notifications require jq — install it with: brew install jq" +} +EOF + exit 0 +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/build-payload.sh" From 5369f3ea1b585142bb0584f90e7b7bc03d2a630d Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Fri, 20 Mar 2026 12:21:30 -0400 Subject: [PATCH 09/16] make language shell-agnostic --- plugins/warp/scripts/on-session-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index b4631a1..746e9d1 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -5,7 +5,7 @@ if ! command -v jq &>/dev/null; then cat << 'EOF' { - "systemMessage": "âš ī¸ Warp notifications require jq — install it with: brew install jq" + "systemMessage": "âš ī¸ Warp notifications require jq — install it with your system package manager (e.g. brew install jq, apt install jq)" } EOF exit 0 From e29c64cc7b0a3f00cc7eee7e73b6a3cf086abf6a Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Fri, 20 Mar 2026 14:31:29 -0400 Subject: [PATCH 10/16] have claude read max accepted client version and use it when sending its payload --- plugins/warp/scripts/build-payload.sh | 20 +++++++++++++++++- plugins/warp/tests/test-hooks.sh | 30 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/plugins/warp/scripts/build-payload.sh b/plugins/warp/scripts/build-payload.sh index 4c90970..b2fb945 100644 --- a/plugins/warp/scripts/build-payload.sh +++ b/plugins/warp/scripts/build-payload.sh @@ -13,11 +13,28 @@ # The function extracts common fields (session_id, cwd, project) from the # hook's stdin JSON (passed as $1), then merges any extra jq args you pass. +# The maximum protocol version this plugin knows how to produce. +PLUGIN_MAX_PROTOCOL_VERSION=1 + +# Negotiate the protocol version with Warp. +# Uses min(plugin_max, warp_declared), falling back to 1 if Warp doesn't advertise a version. +negotiate_protocol_version() { + local warp_version="${WARP_CLI_AGENT_PROTOCOL_VERSION:-1}" + if [ "$warp_version" -lt "$PLUGIN_MAX_PROTOCOL_VERSION" ] 2>/dev/null; then + echo "$warp_version" + else + echo "$PLUGIN_MAX_PROTOCOL_VERSION" + fi +} + build_payload() { local input="$1" local event="$2" shift 2 + local protocol_version + protocol_version=$(negotiate_protocol_version) + # Extract common fields from the hook input local session_id cwd project session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) @@ -30,11 +47,12 @@ build_payload() { # Build the payload: common fields + any extra args passed by the caller. # Extra args should be jq flag pairs like: --arg key "value" or --argjson key '{"a":1}' jq -nc \ + --argjson v "$protocol_version" \ --arg agent "claude" \ --arg event "$event" \ --arg session_id "$session_id" \ --arg cwd "$cwd" \ --arg project "$project" \ "$@" \ - '{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project} + $ARGS.named' + '{v:$v, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project} + $ARGS.named' } diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 264e692..86720ee 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -112,6 +112,36 @@ PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "stop" \ assert_json_field "quotes in query preserved" "$PAYLOAD" ".query" 'what does "hello world" mean?' assert_json_field "parens in response preserved" "$PAYLOAD" ".response" 'It means greeting. Use: printf("hello")' +echo "" +echo "--- Protocol version negotiation ---" + +# Default: no env var set → falls back to plugin max (1) +unset WARP_CLI_AGENT_PROTOCOL_VERSION +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") +assert_json_field "defaults to v1 when env var absent" "$PAYLOAD" ".v" "1" + +# Warp declares v1 → use 1 +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") +assert_json_field "v1 when warp declares 1" "$PAYLOAD" ".v" "1" + +# Warp declares a higher version than the plugin knows → capped to plugin max +export WARP_CLI_AGENT_PROTOCOL_VERSION=99 +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") +assert_json_field "capped to plugin max when warp is ahead" "$PAYLOAD" ".v" "1" + +# Warp declares a lower version than the plugin knows → use warp's version +# (not testable with PLUGIN_MAX=1 since there's no v0, but we verify the min logic +# by temporarily overriding the variable) +PLUGIN_MAX_PROTOCOL_VERSION=5 +export WARP_CLI_AGENT_PROTOCOL_VERSION=3 +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") +assert_json_field "uses warp version when plugin is ahead" "$PAYLOAD" ".v" "3" +PLUGIN_MAX_PROTOCOL_VERSION=1 + +# Clean up +unset WARP_CLI_AGENT_PROTOCOL_VERSION + # --- Summary --- echo "" From f0ac3a11e0ba8b09ff620e58494836d171bd53dd Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Mon, 23 Mar 2026 10:36:48 -0400 Subject: [PATCH 11/16] rename max to current --- plugins/warp/scripts/build-payload.sh | 10 +++++----- plugins/warp/tests/test-hooks.sh | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/warp/scripts/build-payload.sh b/plugins/warp/scripts/build-payload.sh index b2fb945..9ad610e 100644 --- a/plugins/warp/scripts/build-payload.sh +++ b/plugins/warp/scripts/build-payload.sh @@ -13,17 +13,17 @@ # The function extracts common fields (session_id, cwd, project) from the # hook's stdin JSON (passed as $1), then merges any extra jq args you pass. -# The maximum protocol version this plugin knows how to produce. -PLUGIN_MAX_PROTOCOL_VERSION=1 +# The current protocol version this plugin knows how to produce. +PLUGIN_CURRENT_PROTOCOL_VERSION=1 # Negotiate the protocol version with Warp. -# Uses min(plugin_max, warp_declared), falling back to 1 if Warp doesn't advertise a version. +# Uses min(plugin_current, warp_declared), falling back to 1 if Warp doesn't advertise a version. negotiate_protocol_version() { local warp_version="${WARP_CLI_AGENT_PROTOCOL_VERSION:-1}" - if [ "$warp_version" -lt "$PLUGIN_MAX_PROTOCOL_VERSION" ] 2>/dev/null; then + if [ "$warp_version" -lt "$PLUGIN_CURRENT_PROTOCOL_VERSION" ] 2>/dev/null; then echo "$warp_version" else - echo "$PLUGIN_MAX_PROTOCOL_VERSION" + echo "$PLUGIN_CURRENT_PROTOCOL_VERSION" fi } diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 86720ee..a7e9b2e 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -125,19 +125,19 @@ export WARP_CLI_AGENT_PROTOCOL_VERSION=1 PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") assert_json_field "v1 when warp declares 1" "$PAYLOAD" ".v" "1" -# Warp declares a higher version than the plugin knows → capped to plugin max +# Warp declares a higher version than the plugin knows → capped to plugin current export WARP_CLI_AGENT_PROTOCOL_VERSION=99 PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") -assert_json_field "capped to plugin max when warp is ahead" "$PAYLOAD" ".v" "1" +assert_json_field "capped to plugin current when warp is ahead" "$PAYLOAD" ".v" "1" # Warp declares a lower version than the plugin knows → use warp's version # (not testable with PLUGIN_MAX=1 since there's no v0, but we verify the min logic # by temporarily overriding the variable) -PLUGIN_MAX_PROTOCOL_VERSION=5 +PLUGIN_CURRENT_PROTOCOL_VERSION=5 export WARP_CLI_AGENT_PROTOCOL_VERSION=3 PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp"}' "stop") assert_json_field "uses warp version when plugin is ahead" "$PAYLOAD" ".v" "3" -PLUGIN_MAX_PROTOCOL_VERSION=1 +PLUGIN_CURRENT_PROTOCOL_VERSION=1 # Clean up unset WARP_CLI_AGENT_PROTOCOL_VERSION From 55c1d9e5ab8dedc6be269df62c35dbf171bc266a Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Tue, 24 Mar 2026 10:35:38 -0700 Subject: [PATCH 12/16] auto-discover hooks --- plugins/warp/.claude-plugin/plugin.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/warp/.claude-plugin/plugin.json b/plugins/warp/.claude-plugin/plugin.json index 97b8831..1af1038 100644 --- a/plugins/warp/.claude-plugin/plugin.json +++ b/plugins/warp/.claude-plugin/plugin.json @@ -6,5 +6,6 @@ "name": "Warp", "url": "https://warp.dev" }, - "homepage": "https://github.com/warpdotdev/claude-code-warp" + "homepage": "https://github.com/warpdotdev/claude-code-warp", + "strict": false } From e4364bab22cd1a2ef8462a8607de88f705b7be41 Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Wed, 25 Mar 2026 17:37:22 -0700 Subject: [PATCH 13/16] trust protocol version declaration instead of term program to get plugin working over ssh and stopped for legacy users --- plugins/warp/scripts/on-session-start.sh | 9 +++++++++ plugins/warp/scripts/warp-notify.sh | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index 746e9d1..7066ee8 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -11,6 +11,15 @@ EOF exit 0 fi +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + cat << 'EOF' +{ + "systemMessage": "âš ī¸ Please update Warp to get agent notifications — your terminal does not declare cli-agent protocol support" +} +EOF + exit 0 +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/build-payload.sh" diff --git a/plugins/warp/scripts/warp-notify.sh b/plugins/warp/scripts/warp-notify.sh index b13a71c..d1c1c0c 100755 --- a/plugins/warp/scripts/warp-notify.sh +++ b/plugins/warp/scripts/warp-notify.sh @@ -5,10 +5,10 @@ # For structured Warp notifications, title should be "warp://cli-agent" # and body should be a JSON string matching the cli-agent notification schema. -# Only emit notifications when running in Warp. -# Otherwise, folks that use warp _and_ another terminal will get -# garbled notifications whenever they run claude elsewhere. -if [ "$TERM_PROGRAM" != "WarpTerminal" ]; then +# Only emit notifications when Warp declares protocol support. +# This avoids garbled OSC sequences in non-Warp terminals +# (and works over SSH where TERM_PROGRAM isn't propagated). +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then exit 0 fi From 67e1e6fb688ef7b86ff2526c8793333409755ddb Mon Sep 17 00:00:00 2001 From: harryalbert <65182701+harryalbert@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:21:35 -0400 Subject: [PATCH 14/16] Make plugin work for legacy users as it did before (#6) If we want to release this plugin to the same plugin repo that the old repo was in, we should make sure users who upgrade the plugin but don't update warp don't just loose the functionality. To support this, this PR adds back the legacy plugin's hooks, and we use those hooks when we can see that the user is in warp but their warp version has not declared a version of notifications that it supports (i.e. they haven't updated). This ensures complete backwards compatibility. demo: https://www.loom.com/share/a2f0dd9f94aa4b85881215444a6106d2 --- .../warp/scripts/legacy/on-notification.sh | 14 ++++++ .../warp/scripts/legacy/on-session-start.sh | 20 ++++++++ plugins/warp/scripts/legacy/on-stop.sh | 48 +++++++++++++++++++ plugins/warp/scripts/legacy/warp-notify.sh | 10 ++++ plugins/warp/scripts/on-notification.sh | 7 +++ plugins/warp/scripts/on-permission-request.sh | 6 +++ plugins/warp/scripts/on-post-tool-use.sh | 6 +++ plugins/warp/scripts/on-prompt-submit.sh | 6 +++ plugins/warp/scripts/on-session-start.sh | 18 +++---- plugins/warp/scripts/on-stop.sh | 7 +++ plugins/warp/tests/test-hooks.sh | 34 +++++++++++++ 11 files changed, 165 insertions(+), 11 deletions(-) create mode 100755 plugins/warp/scripts/legacy/on-notification.sh create mode 100755 plugins/warp/scripts/legacy/on-session-start.sh create mode 100755 plugins/warp/scripts/legacy/on-stop.sh create mode 100755 plugins/warp/scripts/legacy/warp-notify.sh diff --git a/plugins/warp/scripts/legacy/on-notification.sh b/plugins/warp/scripts/legacy/on-notification.sh new file mode 100755 index 0000000..378923e --- /dev/null +++ b/plugins/warp/scripts/legacy/on-notification.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Hook script for Claude Code Notification event +# Sends a Warp notification when Claude needs user input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Read hook input from stdin +INPUT=$(cat) + +# Extract the notification message +MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null) +[ -z "$MSG" ] && MSG="Input needed" + +"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG" diff --git a/plugins/warp/scripts/legacy/on-session-start.sh b/plugins/warp/scripts/legacy/on-session-start.sh new file mode 100755 index 0000000..4d5e7fb --- /dev/null +++ b/plugins/warp/scripts/legacy/on-session-start.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Hook script for Claude Code SessionStart event +# Shows welcome message and Warp detection status + +# Check if running in Warp terminal +if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then + # Running in Warp - notifications will work + cat << 'EOF' +{ + "systemMessage": "🔔 Warp plugin active. You'll receive native Warp notifications when tasks complete or input is needed." +} +EOF +else + # Not running in Warp - suggest installing + cat << 'EOF' +{ + "systemMessage": "â„šī¸ Warp plugin installed but you're not running in Warp terminal. Install Warp (https://warp.dev) to get native notifications when Claude completes tasks or needs input." +} +EOF +fi diff --git a/plugins/warp/scripts/legacy/on-stop.sh b/plugins/warp/scripts/legacy/on-stop.sh new file mode 100755 index 0000000..2a45dd9 --- /dev/null +++ b/plugins/warp/scripts/legacy/on-stop.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Hook script for Claude Code Stop event +# Sends a Warp notification when Claude completes a task + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Read hook input from stdin +INPUT=$(cat) + +# Extract transcript path from the hook input +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) + +# Default message +MSG="Task completed" + +# Try to extract prompt and response from the transcript (JSONL format) +if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then + # Get the first user prompt + PROMPT=$(jq -rs ' + [.[] | select(.type == "user")] | first | .message.content // empty + ' "$TRANSCRIPT_PATH" 2>/dev/null) + + # Get the last assistant response + RESPONSE=$(jq -rs ' + [.[] | select(.type == "assistant" and .message.content)] | last | + [.message.content[] | select(.type == "text") | .text] | join(" ") + ' "$TRANSCRIPT_PATH" 2>/dev/null) + + if [ -n "$PROMPT" ] && [ -n "$RESPONSE" ]; then + # Truncate prompt to 50 chars + if [ ${#PROMPT} -gt 50 ]; then + PROMPT="${PROMPT:0:47}..." + fi + # Truncate response to 120 chars + if [ ${#RESPONSE} -gt 120 ]; then + RESPONSE="${RESPONSE:0:117}..." + fi + MSG="\"${PROMPT}\" → ${RESPONSE}" + elif [ -n "$RESPONSE" ]; then + # Fallback to just response if no prompt found + if [ ${#RESPONSE} -gt 175 ]; then + RESPONSE="${RESPONSE:0:172}..." + fi + MSG="$RESPONSE" + fi +fi + +"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG" diff --git a/plugins/warp/scripts/legacy/warp-notify.sh b/plugins/warp/scripts/legacy/warp-notify.sh new file mode 100755 index 0000000..6ca0588 --- /dev/null +++ b/plugins/warp/scripts/legacy/warp-notify.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Warp notification utility using OSC escape sequences +# Usage: warp-notify.sh <title> <body> + +TITLE="${1:-Notification}" +BODY="${2:-}" + +# OSC 777 format: \033]777;notify;<title>;<body>\007 +# Write directly to /dev/tty to ensure it reaches the terminal +printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true diff --git a/plugins/warp/scripts/on-notification.sh b/plugins/warp/scripts/on-notification.sh index 0420d1d..bed315b 100755 --- a/plugins/warp/scripts/on-notification.sh +++ b/plugins/warp/scripts/on-notification.sh @@ -3,6 +3,13 @@ # Sends a structured Warp notification when Claude has been idle SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Legacy fallback for old Warp versions +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + [ "$TERM_PROGRAM" = "WarpTerminal" ] && exec "$SCRIPT_DIR/legacy/on-notification.sh" + exit 0 +fi + source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh index 938cc8a..792c03a 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -3,6 +3,12 @@ # Sends a structured Warp notification when Claude needs permission to run a tool SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# No legacy equivalent for this hook +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + exit 0 +fi + source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/scripts/on-post-tool-use.sh b/plugins/warp/scripts/on-post-tool-use.sh index 574f56a..0903344 100755 --- a/plugins/warp/scripts/on-post-tool-use.sh +++ b/plugins/warp/scripts/on-post-tool-use.sh @@ -4,6 +4,12 @@ # transitioning the session status from Blocked back to Running. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# No legacy equivalent for this hook +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + exit 0 +fi + source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/scripts/on-prompt-submit.sh b/plugins/warp/scripts/on-prompt-submit.sh index 150997e..8152358 100755 --- a/plugins/warp/scripts/on-prompt-submit.sh +++ b/plugins/warp/scripts/on-prompt-submit.sh @@ -4,6 +4,12 @@ # transitioning the session status from idle/blocked back to running. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# No legacy equivalent for this hook +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + exit 0 +fi + source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index 7066ee8..fbc2147 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -2,25 +2,21 @@ # Hook script for Claude Code SessionStart event # Shows welcome message, Warp detection status, and emits plugin version -if ! command -v jq &>/dev/null; then - cat << 'EOF' -{ - "systemMessage": "âš ī¸ Warp notifications require jq — install it with your system package manager (e.g. brew install jq, apt install jq)" -} -EOF - exit 0 -fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Legacy fallback for old Warp versions if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + exec "$SCRIPT_DIR/legacy/on-session-start.sh" +fi + +if ! command -v jq &>/dev/null; then cat << 'EOF' { - "systemMessage": "âš ī¸ Please update Warp to get agent notifications — your terminal does not declare cli-agent protocol support" + "systemMessage": "âš ī¸ Warp notifications require jq — install it with your system package manager (e.g. brew install jq, apt install jq)" } EOF exit 0 fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index 62e0132..f4a77fe 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -3,6 +3,13 @@ # Sends a structured Warp notification when Claude completes a task SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Legacy fallback for old Warp versions +if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then + [ "$TERM_PROGRAM" = "WarpTerminal" ] && exec "$SCRIPT_DIR/legacy/on-stop.sh" + exit 0 +fi + source "$SCRIPT_DIR/build-payload.sh" # Read hook input from stdin diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index a7e9b2e..167e7bc 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -142,6 +142,40 @@ PLUGIN_CURRENT_PROTOCOL_VERSION=1 # Clean up unset WARP_CLI_AGENT_PROTOCOL_VERSION +# --- Routing tests --- +# These test the hook scripts as subprocesses to verify routing behavior. +# We override /dev/tty writes since they'd fail in CI. + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)" + +echo "" +echo "=== Routing ===" + +echo "" +echo "--- SessionStart routing ---" + +# Legacy Warp (TERM_PROGRAM=WarpTerminal, no protocol version) +OUTPUT=$(TERM_PROGRAM=WarpTerminal bash "$HOOK_DIR/on-session-start.sh" < /dev/null 2>/dev/null) +SYS_MSG=$(echo "$OUTPUT" | jq -r '.systemMessage // empty' 2>/dev/null) +assert_eq "legacy Warp shows active message" \ + "🔔 Warp plugin active. You'll receive native Warp notifications when tasks complete or input is needed." \ + "$SYS_MSG" + +# Not Warp (neither env var set) +OUTPUT=$(TERM_PROGRAM=other bash "$HOOK_DIR/on-session-start.sh" < /dev/null 2>/dev/null) +SYS_MSG=$(echo "$OUTPUT" | jq -r '.systemMessage // empty' 2>/dev/null) +assert_eq "non-Warp shows install message" \ + "â„šī¸ Warp plugin installed but you're not running in Warp terminal. Install Warp (https://warp.dev) to get native notifications when Claude completes tasks or needs input." \ + "$SYS_MSG" + +echo "" +echo "--- Modern-only hooks exit silently without protocol version ---" + +for HOOK in on-permission-request.sh on-prompt-submit.sh on-post-tool-use.sh; do + echo '{}' | bash "$HOOK_DIR/$HOOK" 2>/dev/null + assert_eq "$HOOK exits 0 without protocol version" "0" "$?" +done + # --- Summary --- echo "" From eef4c5ce08b95b05788518fdb997584c692b5e4c Mon Sep 17 00:00:00 2001 From: harryalbert <65182701+harryalbert@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:29:21 -0400 Subject: [PATCH 15/16] update README (#7) WISOTT. This needs to be updated for the new hooks and functionality --- .claude-plugin/marketplace.json | 2 +- README.md | 52 +++++++++++++------------ plugins/warp/.claude-plugin/plugin.json | 2 +- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8df5d11..a265fe6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "warp", "description": "Native Warp notifications when Claude completes tasks or needs input", "source": "./plugins/warp", - "version": "1.1.0", + "version": "2.0.0", "category": "productivity", "tags": ["notifications", "terminal", "warp"] } diff --git a/README.md b/README.md index d96bf27..6bc7f70 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,18 @@ Official [Warp](https://warp.dev) terminal integration for [Claude Code](https:/ Get native Warp notifications when Claude Code: - **Completes a task** — with a summary showing your prompt and Claude's response -- **Needs your input** — when Claude requires approval or has a question +- **Needs your input** — when Claude has been idle and is waiting for you +- **Requests permission** — when Claude wants to run a tool and needs your approval Notifications appear in Warp's notification center and as system notifications, so you can context-switch while Claude works and get alerted when attention is needed. -**Example notification:** -``` -"what's 1+1" → 2 -``` +### 📡 Session Status + +The plugin keeps Warp informed of Claude's current state by emitting structured events on every session transition: +- **Prompt submitted** — you sent a prompt, Claude is working +- **Tool completed** — a tool call finished, Claude is back to running + +This powers Warp's inline status indicators for Claude Code sessions. ## Installation @@ -27,9 +31,9 @@ Notifications appear in Warp's notification center and as system notifications, /plugin install warp@claude-code-warp ``` -> âš ī¸ **Important**: After installing, **restart Claude Code** for notifications to activate. +> âš ī¸ **Important**: After installing, **restart Claude Code or run /reload-plugins** for the plugin to activate. -Once restarted, you'll see a confirmation message and notifications will appear automatically when Claude completes tasks. +Once restarted, you'll see a confirmation message and notifications will appear automatically. ## Requirements @@ -39,27 +43,26 @@ Once restarted, you'll see a confirmation message and notifications will appear ## How It Works -This plugin uses Warp's [pluggable notifications](https://docs.warp.dev/features/notifications) feature via OSC escape sequences. When Claude Code triggers a hook event, the plugin: +The plugin communicates with Warp via OSC 777 escape sequences. Each hook script builds a structured JSON payload (via `build-payload.sh`) and sends it to `warp://cli-agent`, where Warp parses it to drive notifications and session UI. -1. Reads the session transcript to extract your original prompt and Claude's response -2. Formats a concise notification message -3. Sends an OSC 777 escape sequence to Warp, which displays a native notification +Payloads include a protocol version negotiated between the plugin and Warp (`min(plugin_version, warp_version)`), the session ID, working directory, and event-specific fields. -The plugin registers three hooks: -- **SessionStart** — shows a welcome message confirming the plugin is active -- **Stop** — fires when Claude finishes responding -- **Notification** — fires when Claude needs user input +The plugin registers six hooks: +- **SessionStart** — emits the plugin version and a welcome system message +- **Stop** — reads the transcript to extract your prompt and Claude's response, then sends a task-complete notification +- **Notification** (`idle_prompt`) — fires when Claude has been idle and needs your input +- **PermissionRequest** — fires when Claude wants to run a tool, includes the tool name and a preview of its input +- **UserPromptSubmit** — fires when you submit a prompt, signaling the session is active again +- **PostToolUse** — fires when a tool call completes, signaling the session is no longer blocked -## Configuration +### Legacy Support + +Older Warp clients that predate the structured notification protocol are still supported — they receive plain-text notifications for SessionStart, Stop, and Notification hooks. -Notifications work out of the box. To customize Warp's notification behavior (sounds, system notifications, etc.), see [Warp's notification settings](https://docs.warp.dev/features/notifications). -## Roadmap +## Configuration -Future Warp integrations planned: -- Warp AI context sharing -- Warp Drive integration for sharing Claude Code configurations -- Custom slash commands +Notifications work out of the box. To customize Warp's notification behavior (sounds, system notifications, etc.), see [Warp's notification settings](https://docs.warp.dev/features/notifications). ## Uninstall @@ -68,9 +71,10 @@ Future Warp integrations planned: /plugin marketplace remove claude-code-warp ``` -## Contributing +## Versioning -Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/warpdotdev/claude-code-warp). +The plugin version in `plugins/warp/.claude-plugin/plugin.json` is checked by the Warp client to detect outdated installations. +When bumping the version here, also update `MINIMUM_PLUGIN_VERSION` in the Warp client. ## License diff --git a/plugins/warp/.claude-plugin/plugin.json b/plugins/warp/.claude-plugin/plugin.json index 1af1038..d7799cd 100644 --- a/plugins/warp/.claude-plugin/plugin.json +++ b/plugins/warp/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "warp", "description": "Warp terminal integration for Claude Code - native notifications, and more to come", - "version": "1.1.0", + "version": "2.0.0", "author": { "name": "Warp", "url": "https://warp.dev" From 60de9e2857d3a1d9acc75ecac47b3103cbf4392a Mon Sep 17 00:00:00 2001 From: Harry <harryalbert364@gmail.com> Date: Mon, 30 Mar 2026 17:01:26 -0400 Subject: [PATCH 16/16] add client version check to fix bad client version --- plugins/warp/scripts/on-notification.sh | 3 +- plugins/warp/scripts/on-permission-request.sh | 3 +- plugins/warp/scripts/on-post-tool-use.sh | 3 +- plugins/warp/scripts/on-prompt-submit.sh | 3 +- plugins/warp/scripts/on-session-start.sh | 3 +- plugins/warp/scripts/on-stop.sh | 3 +- plugins/warp/scripts/should-use-structured.sh | 44 +++++++++++++++ plugins/warp/scripts/warp-notify.sh | 9 ++-- plugins/warp/tests/test-hooks.sh | 54 +++++++++++++++++++ 9 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 plugins/warp/scripts/should-use-structured.sh diff --git a/plugins/warp/scripts/on-notification.sh b/plugins/warp/scripts/on-notification.sh index bed315b..8518ac1 100755 --- a/plugins/warp/scripts/on-notification.sh +++ b/plugins/warp/scripts/on-notification.sh @@ -3,9 +3,10 @@ # Sends a structured Warp notification when Claude has been idle SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # Legacy fallback for old Warp versions -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then [ "$TERM_PROGRAM" = "WarpTerminal" ] && exec "$SCRIPT_DIR/legacy/on-notification.sh" exit 0 fi diff --git a/plugins/warp/scripts/on-permission-request.sh b/plugins/warp/scripts/on-permission-request.sh index 792c03a..7d46ed2 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -3,9 +3,10 @@ # Sends a structured Warp notification when Claude needs permission to run a tool SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # No legacy equivalent for this hook -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then exit 0 fi diff --git a/plugins/warp/scripts/on-post-tool-use.sh b/plugins/warp/scripts/on-post-tool-use.sh index 0903344..568e5b3 100755 --- a/plugins/warp/scripts/on-post-tool-use.sh +++ b/plugins/warp/scripts/on-post-tool-use.sh @@ -4,9 +4,10 @@ # transitioning the session status from Blocked back to Running. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # No legacy equivalent for this hook -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then exit 0 fi diff --git a/plugins/warp/scripts/on-prompt-submit.sh b/plugins/warp/scripts/on-prompt-submit.sh index 8152358..0a8a55e 100755 --- a/plugins/warp/scripts/on-prompt-submit.sh +++ b/plugins/warp/scripts/on-prompt-submit.sh @@ -4,9 +4,10 @@ # transitioning the session status from idle/blocked back to running. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # No legacy equivalent for this hook -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then exit 0 fi diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index fbc2147..b907979 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -3,9 +3,10 @@ # Shows welcome message, Warp detection status, and emits plugin version SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # Legacy fallback for old Warp versions -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then exec "$SCRIPT_DIR/legacy/on-session-start.sh" fi diff --git a/plugins/warp/scripts/on-stop.sh b/plugins/warp/scripts/on-stop.sh index f4a77fe..4163bb9 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -3,9 +3,10 @@ # Sends a structured Warp notification when Claude completes a task SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" # Legacy fallback for old Warp versions -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +if ! should_use_structured; then [ "$TERM_PROGRAM" = "WarpTerminal" ] && exec "$SCRIPT_DIR/legacy/on-stop.sh" exit 0 fi diff --git a/plugins/warp/scripts/should-use-structured.sh b/plugins/warp/scripts/should-use-structured.sh new file mode 100644 index 0000000..13360e0 --- /dev/null +++ b/plugins/warp/scripts/should-use-structured.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Determines whether the current Warp build supports structured CLI agent notifications. +# +# Usage: +# source "$SCRIPT_DIR/should-use-structured.sh" +# if should_use_structured; then +# # ... send structured notification +# else +# # ... legacy fallback or exit +# fi +# +# Returns 0 (true) when structured notifications are safe to use, 1 (false) otherwise. + +# Last known Warp release per channel that unconditionally set +# WARP_CLI_AGENT_PROTOCOL_VERSION without gating it behind the +# HOANotifications feature flag. These builds advertise protocol +# support but can't actually render structured notifications. +LAST_BROKEN_DEV="" +LAST_BROKEN_STABLE="v0.2026.03.25.08.24.stable_05" +LAST_BROKEN_PREVIEW="v0.2026.03.25.08.24.preview_05" + +should_use_structured() { + # No protocol version advertised → Warp doesn't know about structured notifications. + [ -z "${WARP_CLI_AGENT_PROTOCOL_VERSION:-}" ] && return 1 + + # No client version available → can't verify this build has the fix. + # (This catches the broken prod release before this was set, but after WARP_CLI_AGENT_PROTOCOL_VERSION was set without a flag check.) + [ -z "${WARP_CLIENT_VERSION:-}" ] && return 1 + + # Check whether this version is at or before the last broken release for its channel. + local threshold="" + case "$WARP_CLIENT_VERSION" in + *dev*) threshold="$LAST_BROKEN_DEV" ;; + *stable*) threshold="$LAST_BROKEN_STABLE" ;; + *preview*) threshold="$LAST_BROKEN_PREVIEW" ;; + esac + + # If we matched a channel and the version is <= the broken threshold, fall back. + if [ -n "$threshold" ] && [[ ! "$WARP_CLIENT_VERSION" > "$threshold" ]]; then + return 1 + fi + + return 0 +} diff --git a/plugins/warp/scripts/warp-notify.sh b/plugins/warp/scripts/warp-notify.sh index d1c1c0c..523f873 100755 --- a/plugins/warp/scripts/warp-notify.sh +++ b/plugins/warp/scripts/warp-notify.sh @@ -5,10 +5,11 @@ # For structured Warp notifications, title should be "warp://cli-agent" # and body should be a JSON string matching the cli-agent notification schema. -# Only emit notifications when Warp declares protocol support. -# This avoids garbled OSC sequences in non-Warp terminals -# (and works over SSH where TERM_PROGRAM isn't propagated). -if [ -z "$WARP_CLI_AGENT_PROTOCOL_VERSION" ]; then +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +# Only emit notifications when we've confirmed the Warp build can render them. +if ! should_use_structured; then exit 0 fi diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 167e7bc..220d3ae 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -142,6 +142,60 @@ PLUGIN_CURRENT_PROTOCOL_VERSION=1 # Clean up unset WARP_CLI_AGENT_PROTOCOL_VERSION +echo "" +echo "=== should-use-structured.sh ===" + +source "$SCRIPT_DIR/../scripts/should-use-structured.sh" + +echo "" +echo "--- No protocol version → legacy ---" +unset WARP_CLI_AGENT_PROTOCOL_VERSION +unset WARP_CLIENT_VERSION +should_use_structured +assert_eq "no protocol version returns false" "1" "$?" + +echo "" +echo "--- Protocol set, no client version → legacy ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +unset WARP_CLIENT_VERSION +should_use_structured +assert_eq "missing WARP_CLIENT_VERSION returns false" "1" "$?" + +echo "" +echo "--- Protocol set, dev version → always structured (dev was never broken) ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +export WARP_CLIENT_VERSION="v0.2026.03.30.08.43.dev_00" +should_use_structured +assert_eq "dev version returns true" "0" "$?" + +echo "" +echo "--- Protocol set, broken stable version → legacy ---" +export WARP_CLIENT_VERSION="v0.2026.03.25.08.24.stable_05" +should_use_structured +assert_eq "exact broken stable version returns false" "1" "$?" + +echo "" +echo "--- Protocol set, newer stable version → structured ---" +export WARP_CLIENT_VERSION="v0.2026.04.01.08.00.stable_00" +should_use_structured +assert_eq "newer stable version returns true" "0" "$?" + +echo "" +echo "--- Protocol set, broken preview version → legacy ---" +export WARP_CLIENT_VERSION="v0.2026.03.25.08.24.preview_05" +should_use_structured +assert_eq "exact broken preview version returns false" "1" "$?" + +echo "" +echo "--- Protocol set, newer preview version → structured ---" +export WARP_CLIENT_VERSION="v0.2026.04.01.08.00.preview_00" +should_use_structured +assert_eq "newer preview version returns true" "0" "$?" + +# Clean up +unset WARP_CLI_AGENT_PROTOCOL_VERSION +unset WARP_CLIENT_VERSION + # --- Routing tests --- # These test the hook scripts as subprocesses to verify routing behavior. # We override /dev/tty writes since they'd fail in CI.