Skip to content

Notifications silently dropped: hooks have no controlling terminal, so warp-notify.sh write to /dev/tty fails #49

@bernardocabral04

Description

@bernardocabral04

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_startstop 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

  1. macOS, Warp with WARP_CLI_AGENT_PROTOCOL_VERSION set.
  2. Install the plugin; run claude; complete any task.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions