Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Payloads include a protocol version negotiated between the plugin and Warp (`min

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
- **Stop** — reads the transcript to extract your prompt and Claude's response, then sends a task-complete notification. Skipped while background sub-agents or shells are still running (`background_tasks` in the hook input, Claude Code ≥ 2.1.145); the Stop that fires once they finish sends the notification instead
- **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
Expand Down
28 changes: 28 additions & 0 deletions plugins/warp/scripts/has-background-work.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Determines whether the Claude Code session still has background work in flight.
#
# Claude Code (>= 2.1.145) includes a `background_tasks` array in Stop hook
# input listing in-flight sub-agents, background shells, monitors, workflows
# and teammates. The main agent's turn ends (firing Stop) while those keep
# running, and the harness re-invokes the main agent — firing Stop again —
# each time one of them completes. Only the Stop with nothing left running is
# a real "task complete".
#
# Usage:
# source "$SCRIPT_DIR/has-background-work.sh"
# if has_background_work "$INPUT"; then
# exit 0 # not done yet — a later Stop will fire when the work finishes
# fi
#
# Returns 0 (true) when at least one background task is still running,
# 1 (false) otherwise. On older Claude Code the field is absent, so this
# returns false and behaviour is unchanged. Only `status == "running"` counts,
# so an unfamiliar status fails open toward notifying rather than staying silent.
has_background_work() {
local input="$1"
local running
running=$(echo "$input" | jq -r '
[.background_tasks[]? | select(.status == "running")] | length
' 2>/dev/null)
[ "${running:-0}" -gt 0 ] 2>/dev/null
}
8 changes: 8 additions & 0 deletions plugins/warp/scripts/legacy/on-stop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
# Sends a Warp notification when Claude completes a task

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../has-background-work.sh"

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

# Skip while background work (sub-agents, background shells, ...) is still
# running — only the final Stop, with nothing left in flight, is a real
# "task complete". See has-background-work.sh.
if has_background_work "$INPUT"; then
exit 0
fi

# Extract transcript path from the hook input
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)

Expand Down
9 changes: 9 additions & 0 deletions plugins/warp/scripts/on-stop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ if ! should_use_structured; then
fi

source "$SCRIPT_DIR/build-payload.sh"
source "$SCRIPT_DIR/has-background-work.sh"

# Read hook input from stdin
INPUT=$(cat)
Expand All @@ -22,6 +23,14 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi

# Skip while background work (sub-agents, background shells, monitors, ...) is
# still running. Stop fires when the main agent's turn ends even though tasks
# are in flight, and again each time one finishes and wakes the main agent.
# Only the final Stop, with nothing left running, is a real "task complete".
if has_background_work "$INPUT"; 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.
Expand Down
53 changes: 53 additions & 0 deletions plugins/warp/tests/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,41 @@ OUTPUT=$(emit_terminal_sequence "test-seq")
assert_json_field "new CC outputs terminalSequence" "$OUTPUT" ".terminalSequence" "test-seq"
unset CLAUDE_CODE_VERSION

echo ""
echo "=== has-background-work.sh ==="

source "$SCRIPT_DIR/../scripts/has-background-work.sh"

echo ""
echo "--- Field absent (older Claude Code) → no background work ---"
has_background_work '{"session_id":"s1"}'
assert_eq "missing background_tasks returns false" "1" "$?"

echo ""
echo "--- Empty array → no background work ---"
has_background_work '{"background_tasks":[]}'
assert_eq "empty background_tasks returns false" "1" "$?"

echo ""
echo "--- Running sub-agent → background work ---"
has_background_work '{"background_tasks":[{"id":"a1","type":"subagent","status":"running","description":"Explore repo","agent_type":"Explore"}]}'
assert_eq "running subagent returns true" "0" "$?"

echo ""
echo "--- Running background shell → background work ---"
has_background_work '{"background_tasks":[{"id":"b1","type":"shell","status":"running","description":"tail logs","command":"tail -f log"}]}'
assert_eq "running shell returns true" "0" "$?"

echo ""
echo "--- Only non-running entries → no background work ---"
has_background_work '{"background_tasks":[{"id":"a1","type":"subagent","status":"completed"}]}'
assert_eq "completed task returns false" "1" "$?"

echo ""
echo "--- Malformed input → fail open ---"
has_background_work 'not json'
assert_eq "invalid JSON returns false" "1" "$?"

# --- Routing tests ---
# These test the hook scripts as subprocesses to verify routing behavior.
# We override /dev/tty writes since they'd fail in CI.
Expand All @@ -279,6 +314,24 @@ 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"

echo ""
echo "--- Stop routing with background tasks ---"

# Structured Warp + Claude Code that supports terminalSequence, so a fired
# notification shows up on stdout instead of /dev/tty.
STOP_ENV="WARP_CLI_AGENT_PROTOCOL_VERSION=1 WARP_CLIENT_VERSION=v0.2026.04.01.08.00.stable_00 CLAUDE_CODE_VERSION=2.1.145"

OUTPUT=$(echo '{"session_id":"s1","cwd":"/tmp","background_tasks":[{"id":"a1","type":"subagent","status":"running"}]}' \
| env $STOP_ENV bash "$HOOK_DIR/on-stop.sh" 2>/dev/null)
assert_eq "on-stop.sh exits 0 while background tasks run" "0" "$?"
assert_eq "on-stop.sh sends nothing while background tasks run" "" "$OUTPUT"

OUTPUT=$(echo '{"session_id":"s1","cwd":"/tmp","background_tasks":[]}' \
| env $STOP_ENV bash "$HOOK_DIR/on-stop.sh" 2>/dev/null)
SEQ=$(echo "$OUTPUT" | jq -r '.terminalSequence // empty' 2>/dev/null)
assert_eq "on-stop.sh notifies once background tasks are done" "stop" \
"$(printf '%s' "$SEQ" | sed -n 's/.*"event":"\([a-z_]*\)".*/\1/p')"

echo ""
echo "--- Modern-only hooks exit silently without protocol version ---"

Expand Down