Skip to content

feat(hooks): v2.0.1 — expand claude code hook coverage#27

Open
yigitkonur wants to merge 5 commits intowarpdotdev:mainfrom
yigitkonur:feat/comprehensive-hook-coverage
Open

feat(hooks): v2.0.1 — expand claude code hook coverage#27
yigitkonur wants to merge 5 commits intowarpdotdev:mainfrom
yigitkonur:feat/comprehensive-hook-coverage

Conversation

@yigitkonur
Copy link
Copy Markdown

@yigitkonur yigitkonur commented Apr 21, 2026

what this is

v2.0.1 — patch on top of v2.0.0 that registers the claude code hooks v2 didn't cover, so the sidebar doesn't stay stuck on running / blocked when the actual agent state has moved on. additive only: every v2.0.0 event is still emitted unchanged, and the new event names (session_end, permission_denied, tool_failed, subagent_start, subagent_stop, compact_start, compact_end, cwd_changed) are silently ignored by older warp clients. marketplace-safe.

retagged from the original v3.0.0 after shipping + live testing — the scope is accumulated bugfixes, not a major release.

five correctness bugs fixed

# bug sidebar effect fix
1 Stop hook slept 0.3s + re-parsed the jsonl transcript for the last assistant message turn completion delayed; transcript race under slow flushes read last_assistant_message directly from stdin. drop sleep + jq tree-walk
2 no SessionEnd hook tab stays on its last state after /clear, --resume, logout new on-session-end.sh emits session_end with a reason field
3 no StopFailure hook rate-limit / auth / billing errors leave the tab on running forever new on-stop-failure.sh emits stop with an error field so old clients still mark done, new clients can render as failed
4 no PermissionDenied hook auto-mode denials silently clear; prior permission_request never resolves, tab sticks on blocked new on-permission-denied.sh emits permission_denied with the classifier's reason
5 SessionStart matcher was startup|resume /clear and /compact regenerate session_id but never fire session_start — sidebar shows stale state matcher extended to startup|resume|clear|compact

additional lifecycle hooks

claude code hook warp event what it buys
SubagentStart / SubagentStop subagent_start / subagent_stop a long Agent call used to look like one opaque tool_complete. now nested progress + final message are visible
PostToolUseFailure tool_failed failed tool calls distinguishable from successful ones
PreCompact / PostCompact compact_start / compact_end compaction no longer appears as a frozen session
CwdChanged cwd_changed sidebar project label refreshes immediately after cd
Elicitation question_asked mcp elicitation reuses opencode's existing event — no new ui on warp's side

context enrichment on existing events

opportunistic fields pulled from hook input and added to the envelope:

  • permission_mode on events that have it — sidebar can differentiate plan / auto / acceptEdits / bypassPermissions
  • model on session_start (e.g. claude-sonnet-4-6)
  • source on session_startstartup / resume / clear / compact
  • tool_preview on tool_complete / tool_failed — 120-char preview of command / file_path / url so the sidebar can show Edit: src/auth.ts instead of just Edit

what this pr does NOT fix

#28 — fresh claude tabs render as ⏳ In progress before any turn starts. three plugin-side workarounds were tested (emit nothing, emit session_start only, emit session_start + follow-up stop) and all three produce a wrong sidebar state. the short version: gemini + factory/droid get clean no-pill rows via warp's binary-name auto-detection; agent:"claude" doesn't have that path, and any osc we emit attaches a default pill. detailed reproduction + proposed fixes live in the issue.

this pr does not attempt that workaround — the in progress state you see after merging is the same one v2.0.0 has today. nothing regresses.

testing

# option A: run the vendored suite on the branch directly
git fetch origin pull/27/head:pr-27-test
git checkout pr-27-test
cd plugins/warp && bash tests/test-hooks.sh
# → 86 passed, 0 failed

# option B: install the side-by-side fork (lives on a separate plugin slug
# so it doesn't collide with warp@claude-code-warp)
/plugin marketplace add yigitkonur/claude-code-warp@testing/side-by-side-marketplace
/plugin install warp-v3@claude-code-warp-v3-testing
# disable one to avoid duplicate notifications:
/plugin disable warp@claude-code-warp

test coverage: envelope shape, protocol version negotiation, legacy/structured routing, all new event payloads, on-prompt-submit.shon-stop.sh temp-file handoff, the stop_hook_active re-entry guard on subagent stop.

marketplace notes

  • version bumped 2.0.02.0.1 (patch, backward-compat)
  • no breaking changes, no renamed event types, no envelope field removals
  • old warp clients that don't know the new event names will silently drop those notifications — the sidebar just misses those states the same way it does today

my fork's testing/side-by-side-marketplace branch keeps the full experimental state (plugins/warp-v3/ with the unscoped attempts at the in progress fix + per-session osc log + ppid-walk tty detection + more). that won't land here — it's strictly for follow-up exploration.

…e directly

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.
…r 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
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/.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.
Copilot AI review requested due to automatic review settings April 21, 2026 16:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Expands the Warp Claude Code plugin from partial to full lifecycle hook coverage by registering additional Claude Code hooks and emitting richer, backward-compatible structured events to keep Warp’s sidebar session state accurate.

Changes:

  • Extend hooks.json from 6 → 16 registered hooks, adding 5s timeouts and async execution for selected end-of-turn hooks.
  • Add new hook scripts for missing lifecycle/error/subagent/compaction/cwd/elicitation events and enrich existing payloads (model/source/permission_mode/tool_preview, etc.).
  • Expand shell test coverage to validate new payload shapes and the prompt temp-file handoff.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
plugins/warp/hooks/hooks.json Registers the full set of Claude Code hooks, adds matcher narrowing, 5s timeouts, and async on stop-related hooks.
plugins/warp/.claude-plugin/plugin.json Bumps plugin version to 3.0.0 and updates plugin description.
.claude-plugin/marketplace.json Updates marketplace metadata (version/description) to 3.0.0.
plugins/warp/scripts/on-stop.sh Uses last_assistant_message and prompt temp-file handoff; includes permission_mode.
plugins/warp/scripts/on-stop-failure.sh Adds StopFailure handling by emitting stop with error fields for backward compatibility.
plugins/warp/scripts/on-session-start.sh Expands matcher coverage (startup/resume/clear/compact), enriches session_start payload, and cleans stale temp state.
plugins/warp/scripts/on-session-end.sh Adds session_end emission and session-scoped temp-file cleanup.
plugins/warp/scripts/on-prompt-submit.sh Emits prompt_submit with permission_mode and persists full prompt to a session-scoped temp file.
plugins/warp/scripts/on-permission-request.sh Enriches permission_request with generalized tool preview and permission_mode.
plugins/warp/scripts/on-permission-denied.sh Adds permission_denied emission with reason + mirrored summary formatting.
plugins/warp/scripts/on-post-tool-use.sh Adds tool_preview + permission_mode to tool_complete for state-transition tools.
plugins/warp/scripts/on-post-tool-use-failure.sh Adds tool_failed emission with error/tool preview and interrupt flag.
plugins/warp/scripts/on-subagent-start.sh Adds subagent_start emission for nested agent visibility.
plugins/warp/scripts/on-subagent-stop.sh Adds subagent_stop emission with nested agent final response (async).
plugins/warp/scripts/on-pre-compact.sh Emits compact_start to surface compaction in the sidebar.
plugins/warp/scripts/on-post-compact.sh Emits compact_end to clear compaction state.
plugins/warp/scripts/on-cwd-changed.sh Emits cwd_changed so the sidebar project label updates immediately.
plugins/warp/scripts/on-elicitation.sh Maps Claude elicitation to OpenCode-compatible question_asked.
plugins/warp/scripts/on-notification.sh Passes through title and uses notification type as event name.
plugins/warp/tests/test-hooks.sh Adds tests for new payload shapes, new hook scripts’ silent-exit behavior, and prompt temp-file lifecycle.
README.md Documents v3 lifecycle coverage, hook inventory, and enriched payload envelope.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tool_preview extraction only considers command, file_path, url, and description. Since PostToolUseFailure can fire for tools like WebSearch/Grep that use query/pattern, the preview may be empty even though the input has a good human-readable field. Consider matching the richer heuristic used in permission hooks (include query/pattern and a tostring fallback) to keep previews useful across tool types.

Suggested change
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)
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 elif .query then .query elif .pattern then .pattern else tostring end) // ""' 2>/dev/null)

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
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.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README lists the new Warp events added in v3.0.0 but omits question_asked, even though the hook inventory/table above documents Elicitation mapping to question_asked. For consistency and to avoid confusion for Warp-side routing work, consider including question_asked in that v3.0.0 new-events list (or clarifying that it reuses an existing event name).

Suggested change
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 v3.0.0 adds new Warp events (`session_end`, `permission_denied`, `tool_failed`, `subagent_start`, `subagent_stop`, `compact_start`, `compact_end`, `cwd_changed`, `question_asked`). 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.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-session prompt cache is written to a predictable path in $TMPDIR. In a world-writable temp dir, another local user could pre-create that path as a symlink and cause this script to overwrite an arbitrary file owned by the current user. Consider writing under a per-user 0700 state dir (e.g. $XDG_RUNTIME_DIR/warp-claude-$UID or a securely-created dir) and/or refusing to write if the target exists and is not a regular file you own.

Suggested change
printf '%s' "$FULL_QUERY" > "$QUERY_FILE" 2>/dev/null || true
FULL_QUERY="$FULL_QUERY" QUERY_FILE="$QUERY_FILE" python3 - <<'PY' 2>/dev/null || true
import os
import stat
import sys
path = os.environ["QUERY_FILE"]
data = os.environ.get("FULL_QUERY", "")
try:
st = os.lstat(path)
except FileNotFoundError:
st = None
except OSError:
sys.exit(1)
if st is not None and (not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid()):
sys.exit(1)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
fd = os.open(path, flags, 0o600)
except OSError:
sys.exit(1)
try:
fst = os.fstat(fd)
if not stat.S_ISREG(fst.st_mode) or fst.st_uid != os.getuid():
sys.exit(1)
os.write(fd, data.encode())
finally:
os.close(fd)
PY

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +34
"$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
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup of the per-session temp files happens after emitting the notification. If the /dev/tty write blocks and the 5s hook timeout kills the script, these files may never be removed. Consider deleting the temp files before notifying, or adding a trap so cleanup runs even if notify fails/timeouts.

Copilot uses AI. Check for mistakes.

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)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IS_INTERRUPT comes from jq -r ... 2>/dev/null; if parsing fails it may be empty, and then --argjson is_interrupt "$IS_INTERRUPT" will cause jq to error and the hook to emit nothing. Ensure IS_INTERRUPT is always a valid JSON boolean (true/false) before passing it to --argjson (e.g., default to false when empty).

Suggested change
IS_INTERRUPT=$(echo "$INPUT" | jq -r '.is_interrupt // false' 2>/dev/null)
IS_INTERRUPT=$(echo "$INPUT" | jq -r '.is_interrupt // false' 2>/dev/null)
case "$IS_INTERRUPT" in
true|false)
;;
*)
IS_INTERRUPT=false
;;
esac

Copilot uses AI. Check for mistakes.
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.
@yigitkonur yigitkonur changed the title feat(hooks): complete claude code lifecycle coverage feat(hooks): v2.0.1 — expand claude code hook coverage Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants