Skip to content

Commit 71c64d1

Browse files
authored
Merge pull request #1 from warpdotdev/harry/extend-warp-claude-plugin
extend plugin to include more detailed hooks and structured responses
2 parents 26ef589 + 9b1f308 commit 71c64d1

11 files changed

Lines changed: 364 additions & 51 deletions

File tree

.github/workflows/test.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Plugin Tests
2+
on:
3+
pull_request:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
plugin-tests:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- name: Run tests
13+
# Auto-discovers and runs all test-*.sh scripts under any tests/ directory.
14+
# To add a new test, just drop a test-*.sh file in a tests/ folder.
15+
run: |
16+
shopt -s globstar nullglob
17+
failed=0
18+
for f in **/tests/test-*.sh; do
19+
echo "--- $f ---"
20+
bash "$f" || failed=1
21+
done
22+
exit $failed

plugins/warp/hooks/hooks.json

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,44 @@
2424
],
2525
"Notification": [
2626
{
27-
"matcher": "*",
27+
"matcher": "idle_prompt",
2828
"hooks": [
2929
{
3030
"type": "command",
3131
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-notification.sh"
3232
}
3333
]
3434
}
35+
],
36+
"PermissionRequest": [
37+
{
38+
"hooks": [
39+
{
40+
"type": "command",
41+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-permission-request.sh"
42+
}
43+
]
44+
}
45+
],
46+
"UserPromptSubmit": [
47+
{
48+
"hooks": [
49+
{
50+
"type": "command",
51+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-prompt-submit.sh"
52+
}
53+
]
54+
}
55+
],
56+
"PostToolUse": [
57+
{
58+
"hooks": [
59+
{
60+
"type": "command",
61+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-post-tool-use.sh"
62+
}
63+
]
64+
}
3565
]
3666
}
3767
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# Builds a structured JSON notification payload for warp://cli-agent.
3+
#
4+
# Usage: source this file, then call build_payload with event-specific fields.
5+
#
6+
# Example:
7+
# source "$(dirname "${BASH_SOURCE[0]}")/build-payload.sh"
8+
# BODY=$(build_payload "$INPUT" "stop" \
9+
# --arg query "$QUERY" \
10+
# --arg response "$RESPONSE" \
11+
# --arg transcript_path "$TRANSCRIPT_PATH")
12+
#
13+
# The function extracts common fields (session_id, cwd, project) from the
14+
# hook's stdin JSON (passed as $1), then merges any extra jq args you pass.
15+
16+
build_payload() {
17+
local input="$1"
18+
local event="$2"
19+
shift 2
20+
21+
# Extract common fields from the hook input
22+
local session_id cwd project
23+
session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null)
24+
cwd=$(echo "$input" | jq -r '.cwd // empty' 2>/dev/null)
25+
project=""
26+
if [ -n "$cwd" ]; then
27+
project=$(basename "$cwd")
28+
fi
29+
30+
# Build the payload: common fields + any extra args passed by the caller.
31+
# Extra args should be jq flag pairs like: --arg key "value" or --argjson key '{"a":1}'
32+
jq -nc \
33+
--arg agent "claude" \
34+
--arg event "$event" \
35+
--arg session_id "$session_id" \
36+
--arg cwd "$cwd" \
37+
--arg project "$project" \
38+
"$@" \
39+
'{v:1, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project} + $ARGS.named'
40+
}
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
#!/bin/bash
2-
# Hook script for Claude Code Notification event
3-
# Sends a Warp notification when Claude needs user input
2+
# Hook script for Claude Code Notification event (idle_prompt only)
3+
# Sends a structured Warp notification when Claude has been idle
44

55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/build-payload.sh"
67

78
# Read hook input from stdin
89
INPUT=$(cat)
910

10-
# Extract the notification message
11+
# Extract notification-specific fields
12+
NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null)
1113
MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null)
1214
[ -z "$MSG" ] && MSG="Input needed"
1315

14-
"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG"
16+
BODY=$(build_payload "$INPUT" "$NOTIF_TYPE" \
17+
--arg summary "$MSG")
18+
19+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
# Hook script for Claude Code PermissionRequest event
3+
# Sends a structured Warp notification when Claude needs permission to run a tool
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/build-payload.sh"
7+
8+
# Read hook input from stdin
9+
INPUT=$(cat)
10+
11+
# Extract permission-request-specific fields
12+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null)
13+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null)
14+
# Fallback to empty object if jq failed or returned empty
15+
[ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}'
16+
17+
# Build a human-readable summary
18+
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)
19+
SUMMARY="Wants to run $TOOL_NAME"
20+
if [ -n "$TOOL_PREVIEW" ]; then
21+
if [ ${#TOOL_PREVIEW} -gt 120 ]; then
22+
TOOL_PREVIEW="${TOOL_PREVIEW:0:117}..."
23+
fi
24+
SUMMARY="$SUMMARY: $TOOL_PREVIEW"
25+
fi
26+
27+
BODY=$(build_payload "$INPUT" "permission_request" \
28+
--arg summary "$SUMMARY" \
29+
--arg tool_name "$TOOL_NAME" \
30+
--argjson tool_input "$TOOL_INPUT")
31+
32+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
# Hook script for Claude Code PostToolUse event
3+
# Sends a structured Warp notification after a tool call completes,
4+
# transitioning the session status from Blocked back to Running.
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
source "$SCRIPT_DIR/build-payload.sh"
8+
9+
# Read hook input from stdin
10+
INPUT=$(cat)
11+
12+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
13+
14+
BODY=$(build_payload "$INPUT" "tool_complete" \
15+
--arg tool_name "$TOOL_NAME")
16+
17+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
# Hook script for Claude Code UserPromptSubmit event
3+
# Sends a structured Warp notification when the user submits a prompt,
4+
# transitioning the session status from idle/blocked back to running.
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
source "$SCRIPT_DIR/build-payload.sh"
8+
9+
# Read hook input from stdin
10+
INPUT=$(cat)
11+
12+
# Extract the user's prompt
13+
QUERY=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
14+
if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then
15+
QUERY="${QUERY:0:197}..."
16+
fi
17+
18+
BODY=$(build_payload "$INPUT" "prompt_submit" \
19+
--arg query "$QUERY")
20+
21+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
#!/bin/bash
22
# Hook script for Claude Code SessionStart event
3-
# Shows welcome message and Warp detection status
3+
# Shows welcome message, Warp detection status, and emits plugin version
44

5-
# Check if running in Warp terminal
6-
if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then
7-
# Running in Warp - notifications will work
8-
cat << 'EOF'
9-
{
10-
"systemMessage": "🔔 Warp plugin active. You'll receive native Warp notifications when tasks complete or input is needed."
11-
}
12-
EOF
13-
else
14-
# Not running in Warp - suggest installing
15-
cat << 'EOF'
16-
{
17-
"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."
18-
}
19-
EOF
20-
fi
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/build-payload.sh"
7+
8+
# Read hook input from stdin
9+
INPUT=$(cat)
10+
11+
# Read plugin version from plugin.json
12+
PLUGIN_VERSION=$(jq -r '.version // "unknown"' "$SCRIPT_DIR/../.claude-plugin/plugin.json" 2>/dev/null)
13+
14+
# Emit structured notification with plugin version so Warp can track it
15+
BODY=$(build_payload "$INPUT" "session_start" \
16+
--arg plugin_version "$PLUGIN_VERSION")
17+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"

plugins/warp/scripts/on-stop.sh

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,65 @@
11
#!/bin/bash
22
# Hook script for Claude Code Stop event
3-
# Sends a Warp notification when Claude completes a task
3+
# Sends a structured Warp notification when Claude completes a task
44

55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/build-payload.sh"
67

78
# Read hook input from stdin
89
INPUT=$(cat)
910

10-
# Extract transcript path from the hook input
11-
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
12-
13-
# Default message
14-
MSG="Task completed"
11+
# Skip if a stop hook is already active (prevents double-notification)
12+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
13+
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
14+
exit 0
15+
fi
1516

16-
# Try to extract prompt and response from the transcript (JSONL format)
17+
# Extract the last user prompt and assistant response from the transcript.
18+
# Small delay to allow Claude Code to flush the current turn to the transcript file.
19+
# The Stop hook fires before the transcript is fully written.
20+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
21+
sleep 0.3
22+
QUERY=""
23+
RESPONSE=""
1724
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
18-
# Get the first user prompt
19-
PROMPT=$(jq -rs '
20-
[.[] | select(.type == "user")] | first | .message.content // empty
25+
# Get the last human prompt from the transcript.
26+
# "user" type messages include both human prompts and tool-result messages.
27+
# Human prompts have content that is either a plain string or an array
28+
# containing {type:"text"} blocks. Tool-result messages have content arrays
29+
# containing only {type:"tool_result"} blocks. We filter to messages that
30+
# have at least one "text" block (or are a plain string).
31+
QUERY=$(jq -rs '
32+
[
33+
.[] | select(.type == "user") |
34+
if .message.content | type == "string" then .
35+
elif [.message.content[] | select(.type == "text")] | length > 0 then .
36+
else empty
37+
end
38+
] | last |
39+
if .message.content | type == "array"
40+
then [.message.content[] | select(.type == "text") | .text] | join(" ")
41+
else .message.content // empty
42+
end
2143
' "$TRANSCRIPT_PATH" 2>/dev/null)
22-
44+
2345
# Get the last assistant response
2446
RESPONSE=$(jq -rs '
2547
[.[] | select(.type == "assistant" and .message.content)] | last |
2648
[.message.content[] | select(.type == "text") | .text] | join(" ")
2749
' "$TRANSCRIPT_PATH" 2>/dev/null)
28-
29-
if [ -n "$PROMPT" ] && [ -n "$RESPONSE" ]; then
30-
# Truncate prompt to 50 chars
31-
if [ ${#PROMPT} -gt 50 ]; then
32-
PROMPT="${PROMPT:0:47}..."
33-
fi
34-
# Truncate response to 120 chars
35-
if [ ${#RESPONSE} -gt 120 ]; then
36-
RESPONSE="${RESPONSE:0:117}..."
37-
fi
38-
MSG="\"${PROMPT}\"${RESPONSE}"
39-
elif [ -n "$RESPONSE" ]; then
40-
# Fallback to just response if no prompt found
41-
if [ ${#RESPONSE} -gt 175 ]; then
42-
RESPONSE="${RESPONSE:0:172}..."
43-
fi
44-
MSG="$RESPONSE"
50+
51+
# Truncate for notification display
52+
if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then
53+
QUERY="${QUERY:0:197}..."
54+
fi
55+
if [ -n "$RESPONSE" ] && [ ${#RESPONSE} -gt 200 ]; then
56+
RESPONSE="${RESPONSE:0:197}..."
4557
fi
4658
fi
4759

48-
"$SCRIPT_DIR/warp-notify.sh" "Claude Code" "$MSG"
60+
BODY=$(build_payload "$INPUT" "stop" \
61+
--arg query "$QUERY" \
62+
--arg response "$RESPONSE" \
63+
--arg transcript_path "$TRANSCRIPT_PATH")
64+
65+
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"

plugins/warp/scripts/warp-notify.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
#!/bin/bash
22
# Warp notification utility using OSC escape sequences
33
# Usage: warp-notify.sh <title> <body>
4+
#
5+
# For structured Warp notifications, title should be "warp://cli-agent"
6+
# and body should be a JSON string matching the cli-agent notification schema.
7+
8+
# Only emit notifications when running in Warp.
9+
# Otherwise, folks that use warp _and_ another terminal will get
10+
# garbled notifications whenever they run claude elsewhere.
11+
if [ "$TERM_PROGRAM" != "WarpTerminal" ]; then
12+
exit 0
13+
fi
414

515
TITLE="${1:-Notification}"
616
BODY="${2:-}"

0 commit comments

Comments
 (0)