Plugin: warp@claude-code-warp v2.0.0 (commit b8ad3cc6c1e40b2d2a944f900a4ae0904a54dd7f)
Affected file: scripts/warp-notify.sh
Platform: macOS (Darwin 25.3.0), Warp v0.2026.05.13.09.15.stable_03, WARP_CLI_AGENT_PROTOCOL_VERSION=1
Summary
The plugin never delivers any notification (no desktop banner, no in-tab CLI-agent
indicator) because the only delivery path writes to /dev/tty, and Claude Code
spawns hook subprocesses without a controlling terminal. The write fails and is
swallowed by 2>/dev/null || true, so the failure is completely silent — the plugin
appears installed and healthy, the gate passes, valid payloads are built, and nothing
ever reaches Warp.
This is not specific to wrapper/alias launchers; it reproduces with a plain
claude invocation. Any environment where Claude Code runs hooks detached from the
controlling TTY is affected.
Root cause
scripts/warp-notify.sh:
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true
The hook process inherits no controlling terminal from Claude Code, so /dev/tty
is unopenable. 2>/dev/null || true then masks the failure.
Evidence
Diagnostic instrumentation added to warp-notify.sh (logging only, no behavior
change), then a normal task run in real Claude Code sessions. Every hook firing —
session_start, stop, etc., across multiple profiles — logged:
TERM_PROGRAM=WarpTerminal PROT=1 VER=v0.2026.05.13.09.15.stable_03
tty=not a tty
devtty=UNWRITABLE
gate=PASS
title=warp://cli-agent bodylen=164
So: gate passes, payload is correct, but the target terminal is unwritable.
Working reference: emitting the exact same session_start → stop handshake
(schema built by the plugin's own build_payload) from an interactive shell that
does own the Warp pane PTY makes the CLI-agent tab indicator appear correctly.
The only differing variable is the writable terminal device. Because the very first
session_start handshake is also lost, Warp never registers the agent session, so
every subsequent event is orphaned too.
Suggested fix
Keep /dev/tty as the primary path (correct for normal shells), but when it is
unwritable, walk up the process ancestry to the Claude Code process — which does
own the Warp pane's PTY — and write the OSC there:
_warp_resolve_tty() {
if { : > /dev/tty; } 2>/dev/null; then
echo "/dev/tty"; return 0
fi
local pid=$$ hop tty_name dev
for hop in $(seq 1 16); do
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
{ [ -z "$pid" ] || [ "$pid" = "0" ] || [ "$pid" = "1" ]; } && break
tty_name=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
{ [ -z "$tty_name" ] || [ "$tty_name" = "??" ]; } && continue
dev="/dev/$tty_name"
[ -w "$dev" ] && { echo "$dev"; return 0; }
done
return 1
}
TARGET_TTY="$(_warp_resolve_tty)"
[ -n "$TARGET_TTY" ] && \
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > "$TARGET_TTY" 2>/dev/null || true
Verified: in a no-controlling-terminal process (the exact hook condition) this
resolves the Claude Code process's real PTY and the CLI-agent indicator then renders
correctly from the unmodified plugin flow.
Secondary suggestion
Consider not masking the write failure unconditionally — even a one-line stderr or
opt-in debug log would have made this self-diagnosable instead of silent.
Reproduction
- macOS, Warp with
WARP_CLI_AGENT_PROTOCOL_VERSION set.
- Install the plugin; run
claude; complete any task.
- Observe: no notification, no tab indicator. Add a log line before the
/dev/tty
write and observe tty is not a tty / /dev/tty unwritable in the hook.
Plugin:
warp@claude-code-warpv2.0.0 (commitb8ad3cc6c1e40b2d2a944f900a4ae0904a54dd7f)Affected file:
scripts/warp-notify.shPlatform: macOS (Darwin 25.3.0), Warp
v0.2026.05.13.09.15.stable_03,WARP_CLI_AGENT_PROTOCOL_VERSION=1Summary
The plugin never delivers any notification (no desktop banner, no in-tab CLI-agent
indicator) because the only delivery path writes to
/dev/tty, and Claude Codespawns hook subprocesses without a controlling terminal. The write fails and is
swallowed by
2>/dev/null || true, so the failure is completely silent — the pluginappears installed and healthy, the gate passes, valid payloads are built, and nothing
ever reaches Warp.
This is not specific to wrapper/alias launchers; it reproduces with a plain
claudeinvocation. Any environment where Claude Code runs hooks detached from thecontrolling TTY is affected.
Root cause
scripts/warp-notify.sh:The hook process inherits no controlling terminal from Claude Code, so
/dev/ttyis unopenable.
2>/dev/null || truethen masks the failure.Evidence
Diagnostic instrumentation added to
warp-notify.sh(logging only, no behaviorchange), then a normal task run in real Claude Code sessions. Every hook firing —
session_start,stop, etc., across multiple profiles — logged:So: gate passes, payload is correct, but the target terminal is unwritable.
Working reference: emitting the exact same
session_start→stophandshake(schema built by the plugin's own
build_payload) from an interactive shell thatdoes own the Warp pane PTY makes the CLI-agent tab indicator appear correctly.
The only differing variable is the writable terminal device. Because the very first
session_starthandshake is also lost, Warp never registers the agent session, soevery subsequent event is orphaned too.
Suggested fix
Keep
/dev/ttyas the primary path (correct for normal shells), but when it isunwritable, walk up the process ancestry to the Claude Code process — which does
own the Warp pane's PTY — and write the OSC there:
Verified: in a no-controlling-terminal process (the exact hook condition) this
resolves the Claude Code process's real PTY and the CLI-agent indicator then renders
correctly from the unmodified plugin flow.
Secondary suggestion
Consider not masking the write failure unconditionally — even a one-line stderr or
opt-in debug log would have made this self-diagnosable instead of silent.
Reproduction
WARP_CLI_AGENT_PROTOCOL_VERSIONset.claude; complete any task./dev/ttywrite and observe
ttyis not a tty //dev/ttyunwritable in the hook.