From ecd546c1324ec0c7fe5b311f8c59559d2100f1f8 Mon Sep 17 00:00:00 2001 From: Yigit Konur <9989650+yigitkonur@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:58:16 -0700 Subject: [PATCH 1/5] fix(stop-hook): drop 0.3s transcript race, read last_assistant_message directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code's Stop hook input now exposes `last_assistant_message` in the JSON payload itself (see https://docs.anthropic.com/en/docs/claude-code/hooks#stop-input). The previous 0.3s sleep plus JSONL transcript tree-walk was guarding against a race that no longer exists — the field is populated before the hook fires. The user's prompt is recovered from a session-scoped temp file written by on-prompt-submit.sh during UserPromptSubmit, which is more reliable than re-parsing the transcript to find the last "user" message that's not a tool-result block. Net effect: on-stop.sh goes from ~40 lines of fragile jq parsing plus a blocking sleep to a direct stdin read. Combined with `async: true` (added in a follow-up commit), the hook becomes non-blocking. --- plugins/warp/scripts/on-prompt-submit.sh | 25 ++++++--- plugins/warp/scripts/on-stop.sh | 66 +++++++++--------------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/plugins/warp/scripts/on-prompt-submit.sh b/plugins/warp/scripts/on-prompt-submit.sh index 0a8a55e..c662878 100755 --- a/plugins/warp/scripts/on-prompt-submit.sh +++ b/plugins/warp/scripts/on-prompt-submit.sh @@ -1,28 +1,41 @@ #!/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. +# transitioning the session from idle/done → running. +# +# Also persists the full prompt to a session-scoped temp file so the Stop hook +# can reference it in the completion summary without re-parsing the transcript. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#userpromptsubmit-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" -# No legacy equivalent for this hook if ! should_use_structured; then exit 0 fi 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) +FULL_QUERY=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null) +QUERY="$FULL_QUERY" if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then QUERY="${QUERY:0:197}..." fi +# Persist the full prompt so the Stop hook can reconstruct "query → response". +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +if [ -n "$SESSION_ID" ] && [ -n "$FULL_QUERY" ]; then + QUERY_FILE="${TMPDIR:-/tmp}/warp-claude-${SESSION_ID}.query" + printf '%s' "$FULL_QUERY" > "$QUERY_FILE" 2>/dev/null || true +fi + +PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty' 2>/dev/null) + BODY=$(build_payload "$INPUT" "prompt_submit" \ - --arg query "$QUERY") + --arg query "$QUERY" \ + --arg permission_mode "$PERMISSION_MODE") "$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 4163bb9..47adff6 100755 --- a/plugins/warp/scripts/on-stop.sh +++ b/plugins/warp/scripts/on-stop.sh @@ -1,6 +1,12 @@ #!/bin/bash # Hook script for Claude Code Stop event -# Sends a structured Warp notification when Claude completes a task +# Sends a structured Warp notification when Claude completes a turn. +# +# Claude Code's Stop hook input now exposes `last_assistant_message` directly, +# so the previous 0.3s sleep + JSONL transcript parse is obsolete. The user's +# prompt is read from a session-scoped temp file written by on-prompt-submit.sh. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#stop-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" @@ -13,61 +19,39 @@ fi source "$SCRIPT_DIR/build-payload.sh" -# Read hook input from stdin INPUT=$(cat) -# Skip if a stop hook is already active (prevents double-notification) +# stop_hook_active is true when this hook is re-running because a prior Stop +# hook returned decision:"block". Skip to avoid double-notifications. STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null) if [ "$STOP_HOOK_ACTIVE" = "true" ]; then exit 0 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="" -if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then - # 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") | - 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 - 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) +# Claude's final response — directly available in the hook input. +RESPONSE=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null) +if [ -n "$RESPONSE" ] && [ ${#RESPONSE} -gt 200 ]; then + RESPONSE="${RESPONSE:0:197}..." +fi - # Truncate for notification display +# User's last prompt — written by on-prompt-submit.sh to a session-scoped temp file. +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +QUERY_FILE="${TMPDIR:-/tmp}/warp-claude-${SESSION_ID}.query" +QUERY="" +if [ -n "$SESSION_ID" ] && [ -f "$QUERY_FILE" ]; then + QUERY=$(cat "$QUERY_FILE" 2>/dev/null) 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 +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) +PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty' 2>/dev/null) + BODY=$(build_payload "$INPUT" "stop" \ --arg query "$QUERY" \ --arg response "$RESPONSE" \ - --arg transcript_path "$TRANSCRIPT_PATH") + --arg transcript_path "$TRANSCRIPT_PATH" \ + --arg permission_mode "$PERMISSION_MODE") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" From c737e7652ba67f704220b0650a051bdf99aa269b Mon Sep 17 00:00:00 2001 From: Yigit Konur <9989650+yigitkonur@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:58:39 -0700 Subject: [PATCH 2/5] feat(hooks): complete claude code lifecycle coverage with rich sidebar context Extends claude-code-warp from 6 hooks to 16, covering every Claude Code hook event that has sidebar-relevant meaning. Every handler has a 5s timeout; Stop, StopFailure, and SubagentStop are async so the session response isn't blocked on tty writes. New hooks and the Warp events they emit: SessionEnd -> session_end (cleans up zombie sidebar entries after /clear, resume, logout) StopFailure -> stop (with error field; API errors no longer leave sidebar on running) PermissionDenied -> permission_denied (clears blocked state when auto-mode classifier denies) SubagentStart -> subagent_start (nested Agent runs visible) SubagentStop -> subagent_stop (with last_assistant_message for the nested agent's output) PostToolUseFailure -> tool_failed (distinguish tool errors from successes in the sidebar) PreCompact -> compact_start (sidebar shows compacting instead of looking frozen) PostCompact -> compact_end CwdChanged -> cwd_changed (project label updates on cd without waiting for next event) Elicitation -> question_asked (re-uses OpenCode's event, so MCP elicitation gets existing UI) Existing hooks enriched with richer context so the sidebar can render state with high fidelity: SessionStart: matcher extended to startup|resume|clear|compact, payload now carries source/model/permission_mode/agent_type. Also cleans up stale per-session temp files from prior runs. UserPromptSubmit: persists the full prompt to a session-scoped temp file so Stop can include "query -> response" without re-parsing the transcript. Adds permission_mode to payload. PermissionRequest: tool preview now considers url/query/pattern in addition to command/file_path. Adds permission_mode. PostToolUse: matcher narrowed to Bash|Edit|Write|MultiEdit|NotebookEdit|Agent (the state-transition tools). Cheap read-only tools (Read, Glob, Grep, WebFetch) were firing this hook 50-100 times per session, spawning ~500 jq processes for no sidebar gain. Payload now includes tool_preview and permission_mode. Notification: passes through title alongside summary. Stop: now async, emits permission_mode alongside query/response/transcript. Timeouts on every handler (5s) cap the worst case when /dev/tty blocks in containerized/CI environments where WARP_CLI_AGENT_PROTOCOL_VERSION is inherited but the controlling terminal is missing. Previous default was 600s. References: - https://docs.anthropic.com/en/docs/claude-code/hooks - https://docs.warp.dev/features/notifications --- plugins/warp/hooks/hooks.json | 144 ++++++++++++++++-- plugins/warp/scripts/on-cwd-changed.sh | 24 +++ plugins/warp/scripts/on-elicitation.sh | 33 ++++ plugins/warp/scripts/on-notification.sh | 14 +- plugins/warp/scripts/on-permission-denied.sh | 45 ++++++ plugins/warp/scripts/on-permission-request.sh | 17 ++- plugins/warp/scripts/on-post-compact.sh | 24 +++ .../warp/scripts/on-post-tool-use-failure.sh | 40 +++++ plugins/warp/scripts/on-post-tool-use.sh | 26 +++- plugins/warp/scripts/on-pre-compact.sh | 25 +++ plugins/warp/scripts/on-session-end.sh | 34 +++++ plugins/warp/scripts/on-session-start.sh | 28 +++- plugins/warp/scripts/on-stop-failure.sh | 62 ++++++++ plugins/warp/scripts/on-subagent-start.sh | 26 ++++ plugins/warp/scripts/on-subagent-stop.sh | 39 +++++ 15 files changed, 546 insertions(+), 35 deletions(-) create mode 100755 plugins/warp/scripts/on-cwd-changed.sh create mode 100755 plugins/warp/scripts/on-elicitation.sh create mode 100755 plugins/warp/scripts/on-permission-denied.sh create mode 100755 plugins/warp/scripts/on-post-compact.sh create mode 100755 plugins/warp/scripts/on-post-tool-use-failure.sh create mode 100755 plugins/warp/scripts/on-pre-compact.sh create mode 100755 plugins/warp/scripts/on-session-end.sh create mode 100755 plugins/warp/scripts/on-stop-failure.sh create mode 100755 plugins/warp/scripts/on-subagent-start.sh create mode 100755 plugins/warp/scripts/on-subagent-stop.sh diff --git a/plugins/warp/hooks/hooks.json b/plugins/warp/hooks/hooks.json index b2273b4..972c3cf 100644 --- a/plugins/warp/hooks/hooks.json +++ b/plugins/warp/hooks/hooks.json @@ -1,34 +1,36 @@ { - "description": "Warp terminal notifications", + "description": "Warp terminal notifications — complete Claude Code hook lifecycle coverage", "hooks": { "SessionStart": [ { - "matcher": "startup|resume", + "matcher": "startup|resume|clear|compact", "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-session-start.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-session-start.sh", + "timeout": 5 } ] } ], - "Stop": [ + "SessionEnd": [ { "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-stop.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-session-end.sh", + "timeout": 5 } ] } ], - "Notification": [ + "UserPromptSubmit": [ { - "matcher": "idle_prompt", "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-notification.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-prompt-submit.sh", + "timeout": 5 } ] } @@ -38,27 +40,145 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-permission-request.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-permission-request.sh", + "timeout": 5 } ] } ], - "UserPromptSubmit": [ + "PermissionDenied": [ { "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-prompt-submit.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-permission-denied.sh", + "timeout": 5 } ] } ], "PostToolUse": [ + { + "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit|Agent", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-tool-use.sh", + "timeout": 5 + } + ] + } + ], + "PostToolUseFailure": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-tool-use-failure.sh", + "timeout": 5 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-subagent-start.sh", + "timeout": 5 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-subagent-stop.sh", + "timeout": 5, + "async": true + } + ] + } + ], + "Notification": [ + { + "matcher": "idle_prompt", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-notification.sh", + "timeout": 5 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-stop.sh", + "timeout": 5, + "async": true + } + ] + } + ], + "StopFailure": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-stop-failure.sh", + "timeout": 5, + "async": true + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-pre-compact.sh", + "timeout": 5 + } + ] + } + ], + "PostCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-compact.sh", + "timeout": 5 + } + ] + } + ], + "CwdChanged": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-cwd-changed.sh", + "timeout": 5 + } + ] + } + ], + "Elicitation": [ { "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-tool-use.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-elicitation.sh", + "timeout": 5 } ] } diff --git a/plugins/warp/scripts/on-cwd-changed.sh b/plugins/warp/scripts/on-cwd-changed.sh new file mode 100755 index 0000000..4bc2720 --- /dev/null +++ b/plugins/warp/scripts/on-cwd-changed.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Hook script for Claude Code CwdChanged event +# Emits cwd_changed so the sidebar project label reflects cd commands executed +# by Claude. The envelope's `project` field is derived from basename(cwd) at +# notification time — this event pushes the update without waiting for the +# next tool call. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#cwdchanged + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# No extra fields — the envelope already carries the new cwd and derived project. +BODY=$(build_payload "$INPUT" "cwd_changed") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-elicitation.sh b/plugins/warp/scripts/on-elicitation.sh new file mode 100755 index 0000000..6721104 --- /dev/null +++ b/plugins/warp/scripts/on-elicitation.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Hook script for Claude Code Elicitation event +# MCP servers can request user input mid-tool-execution via elicitation. +# This maps directly to OpenCode's `question_asked` event — Warp already has +# UI wired for it, so re-using that event gets MCP elicitation into the +# sidebar with zero new event registration on Warp's side. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#elicitation-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# Elicitation's matcher is the MCP server name; it also appears in tool_name. +SERVER_NAME=$(echo "$INPUT" | jq -r '.server_name // .tool_name // "unknown"' 2>/dev/null) +MESSAGE=$(echo "$INPUT" | jq -r '.message // .requestedSchema.description // empty' 2>/dev/null) + +if [ -n "$MESSAGE" ] && [ ${#MESSAGE} -gt 200 ]; then + MESSAGE="${MESSAGE:0:197}..." +fi + +BODY=$(build_payload "$INPUT" "question_asked" \ + --arg tool_name "$SERVER_NAME" \ + --arg summary "$MESSAGE") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-notification.sh b/plugins/warp/scripts/on-notification.sh index 8518ac1..604bc88 100755 --- a/plugins/warp/scripts/on-notification.sh +++ b/plugins/warp/scripts/on-notification.sh @@ -1,6 +1,10 @@ #!/bin/bash -# Hook script for Claude Code Notification event (idle_prompt only) -# Sends a structured Warp notification when Claude has been idle +# Hook script for Claude Code Notification event +# Sends a structured Warp notification for Claude Code notification types. +# The event name passed to Warp mirrors notification_type from the hook input +# (e.g. idle_prompt, auth_success) so Warp can route them to distinct UI. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#notification-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" @@ -13,15 +17,15 @@ fi source "$SCRIPT_DIR/build-payload.sh" -# Read hook input from stdin INPUT=$(cat) -# Extract notification-specific fields NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null) MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null) [ -z "$MSG" ] && MSG="Input needed" +TITLE=$(echo "$INPUT" | jq -r '.title // empty' 2>/dev/null) BODY=$(build_payload "$INPUT" "$NOTIF_TYPE" \ - --arg summary "$MSG") + --arg summary "$MSG" \ + --arg title "$TITLE") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-permission-denied.sh b/plugins/warp/scripts/on-permission-denied.sh new file mode 100755 index 0000000..4b0da43 --- /dev/null +++ b/plugins/warp/scripts/on-permission-denied.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Hook script for Claude Code PermissionDenied event +# Fires when the auto-mode classifier silently denies a tool call. Without this +# hook, any prior permission_request that the classifier then denies leaves the +# sidebar stuck on "blocked-awaiting-permission" until the next Stop. +# +# Only fires in auto mode (--dangerously-skip-permissions, +# --permission-mode auto, etc). Manual denials do not fire this event. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#permissiondenied-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null) +TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) +[ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}' +REASON=$(echo "$INPUT" | jq -r '.reason // "denied by auto mode classifier"' 2>/dev/null) + +# Mirror the permission_request summary format so the sidebar renders +# consistently — "Auto-denied Bash: rm -rf /tmp". +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path elif .url then .url elif .query then .query elif .pattern then .pattern else (tostring | .[0:80]) end) // ""' 2>/dev/null) +SUMMARY="Auto-denied $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 + +BODY=$(build_payload "$INPUT" "permission_denied" \ + --arg summary "$SUMMARY" \ + --arg tool_name "$TOOL_NAME" \ + --argjson tool_input "$TOOL_INPUT" \ + --arg reason "$REASON") + +"$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 7d46ed2..c52a9a0 100755 --- a/plugins/warp/scripts/on-permission-request.sh +++ b/plugins/warp/scripts/on-permission-request.sh @@ -1,28 +1,26 @@ #!/bin/bash # Hook script for Claude Code PermissionRequest event -# Sends a structured Warp notification when Claude needs permission to run a tool +# Sends a structured Warp notification when Claude needs permission to run a tool. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#permissionrequest-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" -# No legacy equivalent for this hook if ! should_use_structured; then exit 0 fi source "$SCRIPT_DIR/build-payload.sh" -# Read hook input from stdin INPUT=$(cat) -# 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='{}' -# 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) +# Build a human-readable summary from the tool's most visible field. +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path elif .url then .url elif .query then .query elif .pattern then .pattern 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 @@ -31,9 +29,12 @@ if [ -n "$TOOL_PREVIEW" ]; then SUMMARY="$SUMMARY: $TOOL_PREVIEW" fi +PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty' 2>/dev/null) + BODY=$(build_payload "$INPUT" "permission_request" \ --arg summary "$SUMMARY" \ --arg tool_name "$TOOL_NAME" \ - --argjson tool_input "$TOOL_INPUT") + --argjson tool_input "$TOOL_INPUT" \ + --arg permission_mode "$PERMISSION_MODE") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-post-compact.sh b/plugins/warp/scripts/on-post-compact.sh new file mode 100755 index 0000000..8dad824 --- /dev/null +++ b/plugins/warp/scripts/on-post-compact.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Hook script for Claude Code PostCompact event +# Emits compact_end after context compaction completes. Lets the sidebar +# restore normal session display after showing a compacting indicator. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#postcompact + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "unknown"' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "compact_end" \ + --arg trigger "$TRIGGER") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-post-tool-use-failure.sh b/plugins/warp/scripts/on-post-tool-use-failure.sh new file mode 100755 index 0000000..c220ec4 --- /dev/null +++ b/plugins/warp/scripts/on-post-tool-use-failure.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Hook script for Claude Code PostToolUseFailure event +# Emits tool_failed so the sidebar can distinguish failed tool calls (red) from +# successful ones (green). Without this hook, a tool failure looks identical to +# a success in the sidebar — both just transition state. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#posttoolusefailure-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null) +ERROR=$(echo "$INPUT" | jq -r '.error // empty' 2>/dev/null) +IS_INTERRUPT=$(echo "$INPUT" | jq -r '.is_interrupt // false' 2>/dev/null) + +if [ -n "$ERROR" ] && [ ${#ERROR} -gt 200 ]; then + ERROR="${ERROR:0:197}..." +fi + +# Short preview of the failed invocation for the sidebar. +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path elif .url then .url elif .description then .description else "" end) // ""' 2>/dev/null) +if [ -n "$TOOL_PREVIEW" ] && [ ${#TOOL_PREVIEW} -gt 120 ]; then + TOOL_PREVIEW="${TOOL_PREVIEW:0:117}..." +fi + +BODY=$(build_payload "$INPUT" "tool_failed" \ + --arg tool_name "$TOOL_NAME" \ + --arg error "$ERROR" \ + --arg tool_preview "$TOOL_PREVIEW" \ + --argjson is_interrupt "$IS_INTERRUPT") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-post-tool-use.sh b/plugins/warp/scripts/on-post-tool-use.sh index 568e5b3..133147f 100755 --- a/plugins/warp/scripts/on-post-tool-use.sh +++ b/plugins/warp/scripts/on-post-tool-use.sh @@ -1,24 +1,40 @@ #!/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. +# Sends a structured Warp notification after a tool call completes, transitioning +# the session from "blocked-on-tool" → "running". +# +# The matcher in hooks.json narrows this to state-transition tools (Bash, Edit, +# Write, MultiEdit, Agent, NotebookEdit). Cheap read-only tools (Read, Glob, +# Grep, WebFetch) would spawn this hook 50–100 times per session and generate +# sidebar noise without meaningful state change. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#posttooluse-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" -# No legacy equivalent for this hook if ! should_use_structured; then exit 0 fi 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) +# Compact preview of what the tool actually did — lets the sidebar show +# "Edit: src/foo.ts" instead of just "Edit". +TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.tool_input | if .command then .command elif .file_path then .file_path elif .url then .url elif .description then .description else "" end) // ""' 2>/dev/null) +if [ -n "$TOOL_PREVIEW" ] && [ ${#TOOL_PREVIEW} -gt 120 ]; then + TOOL_PREVIEW="${TOOL_PREVIEW:0:117}..." +fi + +PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty' 2>/dev/null) + BODY=$(build_payload "$INPUT" "tool_complete" \ - --arg tool_name "$TOOL_NAME") + --arg tool_name "$TOOL_NAME" \ + --arg tool_preview "$TOOL_PREVIEW" \ + --arg permission_mode "$PERMISSION_MODE") "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-pre-compact.sh b/plugins/warp/scripts/on-pre-compact.sh new file mode 100755 index 0000000..bf3ffe2 --- /dev/null +++ b/plugins/warp/scripts/on-pre-compact.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Hook script for Claude Code PreCompact event +# Emits compact_start so the sidebar can show "compacting..." instead of letting +# the session appear frozen while context compaction runs. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#precompact + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# trigger: "manual" (user ran /compact) or "auto" (context limit hit) +TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "unknown"' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "compact_start" \ + --arg trigger "$TRIGGER") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-session-end.sh b/plugins/warp/scripts/on-session-end.sh new file mode 100755 index 0000000..5f889af --- /dev/null +++ b/plugins/warp/scripts/on-session-end.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Hook script for Claude Code SessionEnd event +# Emits session_end so Warp can archive the sidebar entry instead of leaving +# it stuck in its last known state (done / running / blocked) after termination. +# +# Also cleans up per-session temp files written by on-prompt-submit.sh. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#sessionend + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# reason: "clear", "resume", "logout", "prompt_input_exit", +# "bypass_permissions_disabled", "other" +REASON=$(echo "$INPUT" | jq -r '.reason // "other"' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "session_end" \ + --arg reason "$REASON") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" + +# Housekeeping: remove the per-session query file written by on-prompt-submit.sh +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +if [ -n "$SESSION_ID" ]; then + rm -f "${TMPDIR:-/tmp}/warp-claude-${SESSION_ID}".* 2>/dev/null || true +fi diff --git a/plugins/warp/scripts/on-session-start.sh b/plugins/warp/scripts/on-session-start.sh index ce7364b..2db3397 100755 --- a/plugins/warp/scripts/on-session-start.sh +++ b/plugins/warp/scripts/on-session-start.sh @@ -1,6 +1,9 @@ #!/bin/bash # Hook script for Claude Code SessionStart event -# Shows welcome message, Warp detection status, and emits plugin version +# Emits session_start with plugin version, Claude Code model, permission mode, +# and the source that triggered the start (startup, resume, clear, compact). +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart-input SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" @@ -20,13 +23,28 @@ EOF fi 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 +# SessionStart-specific context — lets Warp disambiguate fresh sessions from +# resumed/cleared/compacted ones, and surface the active model in the sidebar. +SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null) +MODEL=$(echo "$INPUT" | jq -r '.model // empty' 2>/dev/null) +PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty' 2>/dev/null) +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // empty' 2>/dev/null) + +# Clear any leftover per-session temp files from a previous run that ended abruptly. +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +if [ -n "$SESSION_ID" ]; then + rm -f "${TMPDIR:-/tmp}/warp-claude-${SESSION_ID}".* 2>/dev/null || true +fi + BODY=$(build_payload "$INPUT" "session_start" \ - --arg plugin_version "$PLUGIN_VERSION") + --arg plugin_version "$PLUGIN_VERSION" \ + --arg source "$SOURCE" \ + --arg model "$MODEL" \ + --arg permission_mode "$PERMISSION_MODE" \ + --arg agent_type "$AGENT_TYPE") + "$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-stop-failure.sh b/plugins/warp/scripts/on-stop-failure.sh new file mode 100755 index 0000000..bfe1ebe --- /dev/null +++ b/plugins/warp/scripts/on-stop-failure.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Hook script for Claude Code StopFailure event +# Fires instead of Stop when the turn ends because of an API error (rate limit, +# auth, billing, server). Without this hook, Warp's sidebar would stay in +# "running" state forever since `stop` never fires on errors. +# +# Uses the existing `stop` event with an added `error` field so older Warp +# builds still transition the tab to done; new builds can render it as failed. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#stopfailure-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# error: "rate_limit", "authentication_failed", "billing_error", +# "invalid_request", "server_error", "max_output_tokens", "unknown" +ERROR=$(echo "$INPUT" | jq -r '.error // "unknown"' 2>/dev/null) +ERROR_DETAILS=$(echo "$INPUT" | jq -r '.error_details // empty' 2>/dev/null) +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null) + +# Recover the last user prompt from the session-scoped temp file written by +# on-prompt-submit.sh. Provides context for "what was the user doing when this failed". +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) +QUERY_FILE="${TMPDIR:-/tmp}/warp-claude-${SESSION_ID}.query" +QUERY="" +if [ -n "$SESSION_ID" ] && [ -f "$QUERY_FILE" ]; then + QUERY=$(cat "$QUERY_FILE" 2>/dev/null) + if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then + QUERY="${QUERY:0:197}..." + fi +fi + +# Compose a human-readable error line. `last_assistant_message` for StopFailure +# holds the rendered error text, not Claude's output — use it directly. +if [ -n "$LAST_MSG" ]; then + RESPONSE="$LAST_MSG" +else + RESPONSE="Error: $ERROR" + [ -n "$ERROR_DETAILS" ] && RESPONSE="$RESPONSE — $ERROR_DETAILS" +fi +if [ ${#RESPONSE} -gt 200 ]; then + RESPONSE="${RESPONSE:0:197}..." +fi + +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "stop" \ + --arg query "$QUERY" \ + --arg response "$RESPONSE" \ + --arg error "$ERROR" \ + --arg error_details "$ERROR_DETAILS" \ + --arg transcript_path "$TRANSCRIPT_PATH") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-subagent-start.sh b/plugins/warp/scripts/on-subagent-start.sh new file mode 100755 index 0000000..b537892 --- /dev/null +++ b/plugins/warp/scripts/on-subagent-start.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Hook script for Claude Code SubagentStart event +# Emits subagent_start so the sidebar can visualize nested agent runs instead +# of showing a flat "running" state for the whole parent turn. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#subagentstart-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty' 2>/dev/null) +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // "unknown"' 2>/dev/null) + +BODY=$(build_payload "$INPUT" "subagent_start" \ + --arg agent_id "$AGENT_ID" \ + --arg agent_type "$AGENT_TYPE") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" diff --git a/plugins/warp/scripts/on-subagent-stop.sh b/plugins/warp/scripts/on-subagent-stop.sh new file mode 100755 index 0000000..c2804d1 --- /dev/null +++ b/plugins/warp/scripts/on-subagent-stop.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Hook script for Claude Code SubagentStop event +# Emits subagent_stop with the nested agent's final response so the sidebar can +# show the outcome of each subagent, not just the parent Agent tool call. +# +# https://docs.anthropic.com/en/docs/claude-code/hooks#subagentstop-input + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/should-use-structured.sh" + +if ! should_use_structured; then + exit 0 +fi + +source "$SCRIPT_DIR/build-payload.sh" + +INPUT=$(cat) + +# Guard against double-notification on stop-hook re-entry. +STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null) +if [ "$STOP_HOOK_ACTIVE" = "true" ]; then + exit 0 +fi + +AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty' 2>/dev/null) +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // "unknown"' 2>/dev/null) +AGENT_TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.agent_transcript_path // empty' 2>/dev/null) +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null) +if [ -n "$LAST_MSG" ] && [ ${#LAST_MSG} -gt 200 ]; then + LAST_MSG="${LAST_MSG:0:197}..." +fi + +BODY=$(build_payload "$INPUT" "subagent_stop" \ + --arg agent_id "$AGENT_ID" \ + --arg agent_type "$AGENT_TYPE" \ + --arg response "$LAST_MSG" \ + --arg transcript_path "$AGENT_TRANSCRIPT_PATH") + +"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" From 3eb86ad5d9c21aeadfc93484c719d4279c204730 Mon Sep 17 00:00:00 2001 From: Yigit Konur <9989650+yigitkonur@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:58:49 -0700 Subject: [PATCH 3/5] test(hooks): cover all new event payloads and temp-file prompt handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing build_payload-centric test suite with: - Shape assertions for every new event (session_end, stop with error, permission_denied, tool_failed, subagent_start/stop, compact_start/end, cwd_changed, question_asked) plus enriched session_start fields. - Routing tests for all 10 new hook scripts — each must exit 0 when WARP_CLI_AGENT_PROTOCOL_VERSION is unset (non-Warp terminals). - End-to-end test of the prompt handoff: on-prompt-submit.sh persists the full prompt to a session-scoped temp file, and on-session-end.sh cleans it up on termination. Total: 86 passed, 0 failed. Preserves the auto-discovery pattern used by .github/workflows/test.yml (any tests/test-*.sh is picked up). --- plugins/warp/tests/test-hooks.sh | 169 ++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index c8fcbb6..97cbd24 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -218,11 +218,178 @@ assert_eq "legacy Warp shows active message" \ 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 +unset WARP_CLI_AGENT_PROTOCOL_VERSION +unset WARP_CLIENT_VERSION +for HOOK in \ + on-permission-request.sh \ + on-permission-denied.sh \ + on-prompt-submit.sh \ + on-post-tool-use.sh \ + on-post-tool-use-failure.sh \ + on-session-end.sh \ + on-stop-failure.sh \ + on-subagent-start.sh \ + on-subagent-stop.sh \ + on-pre-compact.sh \ + on-post-compact.sh \ + on-cwd-changed.sh \ + on-elicitation.sh; do echo '{}' | bash "$HOOK_DIR/$HOOK" 2>/dev/null assert_eq "$HOOK exits 0 without protocol version" "0" "$?" done +# --- New event payload shapes --- +# Each new event is tested through build_payload (the shared JSON construction +# layer all hook scripts use) since hook scripts themselves write to /dev/tty. + +echo "" +echo "=== new event payloads ===" + +echo "" +echo "--- session_start enriches with source/model/permission_mode ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "session_start" \ + --arg plugin_version "3.0.0" \ + --arg source "resume" \ + --arg model "claude-sonnet-4-6" \ + --arg permission_mode "acceptEdits" \ + --arg agent_type "") +assert_json_field "session_start event name" "$PAYLOAD" ".event" "session_start" +assert_json_field "plugin_version present" "$PAYLOAD" ".plugin_version" "3.0.0" +assert_json_field "source present" "$PAYLOAD" ".source" "resume" +assert_json_field "model present" "$PAYLOAD" ".model" "claude-sonnet-4-6" +assert_json_field "permission_mode present" "$PAYLOAD" ".permission_mode" "acceptEdits" + +echo "" +echo "--- session_end with reason ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "session_end" \ + --arg reason "clear") +assert_json_field "session_end event name" "$PAYLOAD" ".event" "session_end" +assert_json_field "reason present" "$PAYLOAD" ".reason" "clear" + +echo "" +echo "--- stop with error (StopFailure mapping) ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "stop" \ + --arg query "refactor auth" \ + --arg response "API Error: Rate limit reached" \ + --arg error "rate_limit" \ + --arg error_details "429 Too Many Requests" \ + --arg transcript_path "") +assert_json_field "stop event name for failures" "$PAYLOAD" ".event" "stop" +assert_json_field "error field present" "$PAYLOAD" ".error" "rate_limit" +assert_json_field "error_details present" "$PAYLOAD" ".error_details" "429 Too Many Requests" +assert_json_field "response holds error text" "$PAYLOAD" ".response" "API Error: Rate limit reached" + +echo "" +echo "--- permission_denied ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "permission_denied" \ + --arg summary "Auto-denied Bash: rm -rf /" \ + --arg tool_name "Bash" \ + --argjson tool_input '{"command":"rm -rf /"}' \ + --arg reason "command targets a path outside the project") +assert_json_field "permission_denied event name" "$PAYLOAD" ".event" "permission_denied" +assert_json_field "summary present" "$PAYLOAD" ".summary" "Auto-denied Bash: rm -rf /" +assert_json_field "reason present" "$PAYLOAD" ".reason" "command targets a path outside the project" +assert_json_field "tool_input preserved" "$PAYLOAD" ".tool_input.command" "rm -rf /" + +echo "" +echo "--- tool_failed ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "tool_failed" \ + --arg tool_name "Bash" \ + --arg error "Command exited with non-zero status code 1" \ + --arg tool_preview "npm test" \ + --argjson is_interrupt false) +assert_json_field "tool_failed event name" "$PAYLOAD" ".event" "tool_failed" +assert_json_field "tool_name present" "$PAYLOAD" ".tool_name" "Bash" +assert_json_field "error present" "$PAYLOAD" ".error" "Command exited with non-zero status code 1" +assert_json_field "tool_preview present" "$PAYLOAD" ".tool_preview" "npm test" +assert_json_field "is_interrupt is bool" "$PAYLOAD" ".is_interrupt" "false" + +echo "" +echo "--- subagent_start ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "subagent_start" \ + --arg agent_id "agent-abc123" \ + --arg agent_type "Explore") +assert_json_field "subagent_start event name" "$PAYLOAD" ".event" "subagent_start" +assert_json_field "agent_id present" "$PAYLOAD" ".agent_id" "agent-abc123" +assert_json_field "agent_type present" "$PAYLOAD" ".agent_type" "Explore" + +echo "" +echo "--- subagent_stop with response ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "subagent_stop" \ + --arg agent_id "agent-abc123" \ + --arg agent_type "Explore" \ + --arg response "Found 3 potential issues" \ + --arg transcript_path "/tmp/subagent.jsonl") +assert_json_field "subagent_stop event name" "$PAYLOAD" ".event" "subagent_stop" +assert_json_field "response present" "$PAYLOAD" ".response" "Found 3 potential issues" +assert_json_field "agent_type present" "$PAYLOAD" ".agent_type" "Explore" + +echo "" +echo "--- compact_start / compact_end ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "compact_start" \ + --arg trigger "auto") +assert_json_field "compact_start event name" "$PAYLOAD" ".event" "compact_start" +assert_json_field "trigger present" "$PAYLOAD" ".trigger" "auto" + +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "compact_end" \ + --arg trigger "manual") +assert_json_field "compact_end event name" "$PAYLOAD" ".event" "compact_end" + +echo "" +echo "--- cwd_changed uses envelope cwd ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/new/project/path"}' "cwd_changed") +assert_json_field "cwd_changed event name" "$PAYLOAD" ".event" "cwd_changed" +assert_json_field "cwd reflects new dir" "$PAYLOAD" ".cwd" "/new/project/path" +assert_json_field "project is new basename" "$PAYLOAD" ".project" "path" + +echo "" +echo "--- question_asked (MCP elicitation) ---" +PAYLOAD=$(build_payload '{"session_id":"s1","cwd":"/tmp/proj"}' "question_asked" \ + --arg tool_name "github-server" \ + --arg summary "Which repo should I search?") +assert_json_field "question_asked event name" "$PAYLOAD" ".event" "question_asked" +assert_json_field "tool_name holds server name" "$PAYLOAD" ".tool_name" "github-server" +assert_json_field "summary present" "$PAYLOAD" ".summary" "Which repo should I search?" + +# --- Temp-file prompt persistence --- +# The on-prompt-submit → on-stop handoff goes through a session-scoped temp file. +# Verify the file is created and consumed correctly. + +echo "" +echo "=== temp-file prompt persistence ===" + +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +export WARP_CLIENT_VERSION="v0.2099.12.31.23.59.stable_99" + +TEST_SESSION="test-session-$$" +TEST_TMP="${TMPDIR:-/tmp}/warp-claude-${TEST_SESSION}.query" +rm -f "$TEST_TMP" + +# on-prompt-submit should write the full prompt to the temp file +INPUT=$(jq -nc --arg sid "$TEST_SESSION" '{session_id:$sid,cwd:"/tmp/proj",prompt:"refactor the auth module"}') +echo "$INPUT" | bash "$HOOK_DIR/on-prompt-submit.sh" 2>/dev/null >/dev/null + +if [ -f "$TEST_TMP" ]; then + CONTENT=$(cat "$TEST_TMP") + assert_eq "on-prompt-submit writes query file" "refactor the auth module" "$CONTENT" +else + assert_eq "on-prompt-submit writes query file" "refactor the auth module" "" +fi + +# on-session-end should clean up the temp file +INPUT=$(jq -nc --arg sid "$TEST_SESSION" '{session_id:$sid,cwd:"/tmp/proj",reason:"clear"}') +echo "$INPUT" | bash "$HOOK_DIR/on-session-end.sh" 2>/dev/null >/dev/null + +if [ ! -f "$TEST_TMP" ]; then + assert_eq "on-session-end cleans up query file" "cleaned" "cleaned" +else + assert_eq "on-session-end cleans up query file" "cleaned" "still present" + rm -f "$TEST_TMP" +fi + +unset WARP_CLI_AGENT_PROTOCOL_VERSION +unset WARP_CLIENT_VERSION + # --- Summary --- echo "" From a270c3c395d2c5daf0d9f302ab2a3efb26d1dad5 Mon Sep 17 00:00:00 2001 From: Yigit Konur <9989650+yigitkonur@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:59:10 -0700 Subject: [PATCH 4/5] chore(plugin): bump to v3.0.0 and document complete lifecycle coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plugins/warp/.claude-plugin/plugin.json: 2.0.0 -> 3.0.0 - .claude-plugin/marketplace.json: 2.0.0 -> 3.0.0 - README.md: documents all 16 hooks, the Warp events they emit, and the sidebar effect of each. Adds a v3.0.0 compatibility note explaining the new events are forward-compatible — clients that don't recognize them should silently ignore. Per the existing README guidance, bumping the plugin version requires a coordinated update of MINIMUM_PLUGIN_VERSION in the Warp client to surface the outdated-plugin banner for users on old installations. The adapter continues to emit the v2 event set (session_start, prompt_submit, tool_complete, permission_request, idle_prompt, stop) unchanged — no break for existing Warp clients. --- .claude-plugin/marketplace.json | 4 +- README.md | 65 ++++++++++++++++++++----- plugins/warp/.claude-plugin/plugin.json | 4 +- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a265fe6..6ea204c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,9 +8,9 @@ "plugins": [ { "name": "warp", - "description": "Native Warp notifications when Claude completes tasks or needs input", + "description": "Native Warp notifications with complete Claude Code hook lifecycle coverage", "source": "./plugins/warp", - "version": "2.0.0", + "version": "3.0.0", "category": "productivity", "tags": ["notifications", "terminal", "warp"] } diff --git a/README.md b/README.md index 6bc7f70..45380ea 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,28 @@ 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 +- **Completes a subagent** — when a nested Agent tool call finishes, with the subagent's response - **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 +- **Is auto-denied** — when the auto-mode classifier silently denies a tool call +- **Hits an API error** — rate limits, auth failures, billing errors surface in the sidebar instead of silently hanging +- **Asks a question via MCP** — MCP elicitation dialogs are routed through the same `question_asked` event OpenCode uses 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. ### 📡 Session Status -The plugin keeps Warp informed of Claude's current state by emitting structured events on every session transition: +The plugin keeps Warp's sidebar in sync with Claude's lifecycle by emitting structured events on every transition: + +- **Session start / end** — appear and disappear from the sidebar cleanly (`clear`, `resume`, `logout` reasons included) - **Prompt submitted** — you sent a prompt, Claude is working -- **Tool completed** — a tool call finished, Claude is back to running +- **Permission request / denied** — awaiting approval, or silently denied by auto mode +- **Tool complete / failed** — distinguishes successful tool calls from errors +- **Subagent start / stop** — nested Agent runs visible in the sidebar instead of looking like one opaque tool call +- **Compact start / end** — context compaction is surfaced rather than appearing as a frozen session +- **Cwd changed** — sidebar project label updates in real time when Claude runs `cd` -This powers Warp's inline status indicators for Claude Code sessions. +Every event includes rich context: `permission_mode`, `model`, `source`, and event-specific payloads (tool previews, error types, subagent types) so Warp can render state with high fidelity. ## Installation @@ -45,15 +55,46 @@ Once restarted, you'll see a confirmation message and notifications will appear 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. -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. +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. Every handler has a 5-second timeout; `Stop`, `StopFailure`, and `SubagentStop` run async so the session response isn't blocked on tty writes. + +### Hook inventory + +The plugin registers sixteen hooks covering the full [Claude Code hook lifecycle](https://docs.anthropic.com/en/docs/claude-code/hooks): + +| Claude Code hook | Warp event | Sidebar effect | +|---|---|---| +| `SessionStart` (`startup\|resume\|clear\|compact`) | `session_start` | registers/refreshes the sidebar entry; `model` + `source` surface in the UI | +| `SessionEnd` | `session_end` | archives the sidebar entry with termination reason | +| `UserPromptSubmit` | `prompt_submit` | transitions tab idle/done → running | +| `PermissionRequest` | `permission_request` | transitions tab → blocked-awaiting-permission with a rich summary | +| `PermissionDenied` | `permission_denied` | clears the blocked state when auto-mode classifier denies | +| `PostToolUse` (matcher: `Bash\|Edit\|Write\|MultiEdit\|NotebookEdit\|Agent`) | `tool_complete` | transitions tab → running with tool preview | +| `PostToolUseFailure` | `tool_failed` | distinguishes failed tool calls from successful ones | +| `SubagentStart` | `subagent_start` | surfaces nested Agent runs instead of flat "running" state | +| `SubagentStop` | `subagent_stop` | subagent's final response appears in the sidebar | +| `Notification` (`idle_prompt`) | `idle_prompt` | "waiting for input" badge | +| `Stop` | `stop` | transitions tab → done with prompt/response summary | +| `StopFailure` | `stop` (with `error` field) | API errors (rate limits, auth) surface instead of hanging | +| `PreCompact` / `PostCompact` | `compact_start` / `compact_end` | sidebar shows compaction instead of looking frozen | +| `CwdChanged` | `cwd_changed` | project label updates in real time on `cd` | +| `Elicitation` | `question_asked` | MCP elicitation routes through the existing OpenCode-compatible event | + +### Payload envelope + +Every payload carries the six-field common envelope: + +```json +{ + "v": 1, + "agent": "claude", + "event": "", + "session_id": "", + "cwd": "", + "project": "" +} +``` -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 +Plus event-specific fields. Many events also include `permission_mode` (plan / acceptEdits / auto / bypassPermissions / default) so Warp can adapt sidebar rendering to the current mode. `SessionStart` additionally includes `model` and `source`. ### Legacy Support @@ -76,6 +117,8 @@ Notifications work out of the box. To customize Warp's notification behavior (so 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. +Plugin v3.0.0 adds new Warp events (`session_end`, `permission_denied`, `tool_failed`, `subagent_start`, `subagent_stop`, `compact_start`, `compact_end`, `cwd_changed`). Warp clients that don't know these events should silently ignore them; v3-aware clients render them as first-class sidebar states. The adapter continues to emit the existing six events for backward compatibility. + ## License MIT License — see [LICENSE](LICENSE) for details. diff --git a/plugins/warp/.claude-plugin/plugin.json b/plugins/warp/.claude-plugin/plugin.json index da219bb..452539c 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": "2.0.0", + "description": "Warp terminal integration for Claude Code — complete hook lifecycle coverage, structured notifications, and sidebar session state", + "version": "3.0.0", "author": { "name": "Warp", "url": "https://warp.dev" From 256b05792042ce3df8df25a0609b854e094fa8d0 Mon Sep 17 00:00:00 2001 From: Yigit Konur <9989650+yigitkonur@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:42:44 -0700 Subject: [PATCH 5/5] chore(plugin): retag as v2.0.1 instead of v3.0.0 Downgrades the version tag to signal this is a backward-compatible patch on top of v2.0.0, not a major release. All existing v2.0.0 events are emitted unchanged; the new events (`session_end`, `permission_denied`, `tool_failed`, `subagent_start`, `subagent_stop`, `compact_start`, `compact_end`, `cwd_changed`) are purely additive and older Warp clients will ignore them silently. Keeping the version in patch range avoids tripping marketplace discovery or signalling a breaking change that doesn't exist. --- .claude-plugin/marketplace.json | 2 +- README.md | 2 +- plugins/warp/.claude-plugin/plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6ea204c..a108b60 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "warp", "description": "Native Warp notifications with complete Claude Code hook lifecycle coverage", "source": "./plugins/warp", - "version": "3.0.0", + "version": "2.0.1", "category": "productivity", "tags": ["notifications", "terminal", "warp"] } diff --git a/README.md b/README.md index 45380ea..fa94dbe 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Notifications work out of the box. To customize Warp's notification behavior (so 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. -Plugin v3.0.0 adds new Warp events (`session_end`, `permission_denied`, `tool_failed`, `subagent_start`, `subagent_stop`, `compact_start`, `compact_end`, `cwd_changed`). Warp clients that don't know these events should silently ignore them; v3-aware clients render them as first-class sidebar states. The adapter continues to emit the existing six events for backward compatibility. +Plugin v2.0.1 adds new Warp events (`session_end`, `permission_denied`, `tool_failed`, `subagent_start`, `subagent_stop`, `compact_start`, `compact_end`, `cwd_changed`) on top of v2.0.0's six-event baseline. Warp clients that don't know these events should silently ignore them; newer clients render them as first-class sidebar states. All existing v2.0.0 events are emitted unchanged — this is a backward-compatible patch. ## License diff --git a/plugins/warp/.claude-plugin/plugin.json b/plugins/warp/.claude-plugin/plugin.json index 452539c..d34bbe7 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 — complete hook lifecycle coverage, structured notifications, and sidebar session state", - "version": "3.0.0", + "version": "2.0.1", "author": { "name": "Warp", "url": "https://warp.dev"