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
31 changes: 31 additions & 0 deletions plugins/warp/scripts/find-controlling-tty.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# Emit candidate controlling-tty paths by walking up the process tree.
#
# Background: when the hook runs inside a sandbox that detaches the subprocess
# from its controlling terminal (e.g. Claude Code's macOS `sandbox-exec`
# wrapper for Bash tool calls, when `sandbox.enabled` is set), the hook has
# no controlling terminal — opening `/dev/tty` returns ENXIO. To still deliver
# the OSC notification, we walk up the process tree and emit each ancestor's
# tty device path; the caller tries to write to each in turn until one
# succeeds.
#
# Caveats:
# - We do not verify the ancestor is actually Warp. In nested setups (tmux,
# screen, ssh into non-Warp), the OSC may end up in a terminal that ignores
# it. OSC 777 is widely ignored by terminals that don't implement it, so
# the worst case is a silently dropped notification — not corruption.
# - The walk is depth-limited to bound the number of `ps` invocations.

find_candidate_ttys() {
local pid=$PPID
local depth=0
while [[ -n $pid && $pid != 0 && $pid != 1 && $depth -lt 20 ]]; do
local tty
tty=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -n $tty && $tty != "??" ]]; then
printf '/dev/%s\n' "$tty"
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
depth=$((depth + 1))
done
}
19 changes: 17 additions & 2 deletions plugins/warp/scripts/legacy/warp-notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@
# Warp notification utility using OSC escape sequences
# Usage: warp-notify.sh <title> <body>

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../find-controlling-tty.sh"

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

# OSC 777 format: \033]777;notify;<title>;<body>\007
# Write directly to /dev/tty to ensure it reaches the terminal
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true
emit() {
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > "$1" 2>/dev/null
}

# Prefer /dev/tty. If unopenable (e.g. the hook is running inside a sandbox
# that detached the subprocess from its controlling terminal), fall back to
# trying each ancestor process's tty in turn until one accepts the write.
emit /dev/tty && exit 0

while read -r tty; do
emit "$tty" && exit 0
done < <(find_candidate_ttys)

exit 0
17 changes: 15 additions & 2 deletions plugins/warp/scripts/warp-notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/should-use-structured.sh"
source "$SCRIPT_DIR/find-controlling-tty.sh"

# Only emit notifications when we've confirmed the Warp build can render them.
if ! should_use_structured; then
Expand All @@ -17,5 +18,17 @@ TITLE="${1:-Notification}"
BODY="${2:-}"

# OSC 777 format: \033]777;notify;<title>;<body>\007
# Write directly to /dev/tty to ensure it reaches the terminal
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > /dev/tty 2>/dev/null || true
emit() {
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY" > "$1" 2>/dev/null
}

# Prefer /dev/tty. If unopenable (e.g. the hook is running inside a sandbox
# that detached the subprocess from its controlling terminal), fall back to
# trying each ancestor process's tty in turn until one accepts the write.
emit /dev/tty && exit 0

while read -r tty; do
emit "$tty" && exit 0
done < <(find_candidate_ttys)

exit 0
118 changes: 118 additions & 0 deletions plugins/warp/tests/test-find-controlling-tty.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/bin/bash
# Tests for find-controlling-tty.sh
#
# The function walks up the process tree using `ps`. We override `ps` as a
# shell function so the tests don't depend on the actual process tree.

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)"
source "$SCRIPT_DIR/find-controlling-tty.sh"

PASSED=0
FAILED=0

assert_eq() {
local test_name="$1"
local expected="$2"
local actual="$3"
if [[ $expected == "$actual" ]]; then
echo " ✓ $test_name"
PASSED=$((PASSED + 1))
else
echo " ✗ $test_name"
echo " expected: $expected"
echo " actual: $actual"
FAILED=$((FAILED + 1))
fi
}

echo "=== find-controlling-tty.sh ==="

echo ""
echo "--- Emits the parent's tty when it has one ---"
ps() {
case "$*" in
"-o tty= -p $PPID") echo "ttys003" ;;
"-o ppid= -p $PPID") echo "1" ;;
*) command ps "$@" ;;
esac
}
result=$(find_candidate_ttys | tr '\n' ' ' | sed 's/ $//')
assert_eq "parent tty appears as /dev/<name>" "/dev/ttys003" "$result"
unset -f ps

echo ""
echo "--- Skips ancestors with no controlling tty, keeps walking ---"
ps() {
case "$*" in
"-o tty= -p $PPID") echo "??" ;;
"-o tty= -p 77001") echo "??" ;;
"-o tty= -p 77002") echo "ttys004" ;;
"-o ppid= -p $PPID") echo "77001" ;;
"-o ppid= -p 77001") echo "77002" ;;
"-o ppid= -p 77002") echo "1" ;;
*) command ps "$@" ;;
esac
}
result=$(find_candidate_ttys | tr '\n' ' ' | sed 's/ $//')
assert_eq "?? entries are skipped, real tty emitted" "/dev/ttys004" "$result"
unset -f ps

echo ""
echo "--- Emits multiple ancestors if more than one has a tty ---"
# A nested case (e.g. tmux): both inner and outer ancestors have ttys.
ps() {
case "$*" in
"-o tty= -p $PPID") echo "ttys001" ;;
"-o tty= -p 88001") echo "ttys002" ;;
"-o ppid= -p $PPID") echo "88001" ;;
"-o ppid= -p 88001") echo "1" ;;
*) command ps "$@" ;;
esac
}
result=$(find_candidate_ttys | tr '\n' ' ' | sed 's/ $//')
assert_eq "both ancestor ttys are emitted in order" "/dev/ttys001 /dev/ttys002" "$result"
unset -f ps

echo ""
echo "--- Emits nothing when no ancestor has a tty ---"
ps() {
case "$*" in
"-o tty="*) echo "??" ;;
"-o ppid="*) echo "1" ;;
*) command ps "$@" ;;
esac
}
result=$(find_candidate_ttys)
assert_eq "no output when every ancestor reports ??" "" "$result"
unset -f ps

echo ""
echo "--- Honors depth limit on unbounded process chains ---"
ps() {
case "$*" in
"-o tty="*) echo "??" ;;
"-o ppid="*) echo "9999" ;;
*) command ps "$@" ;;
esac
}
# Should terminate (not hang) even though every ppid points to a non-init pid.
start_secs=$SECONDS
find_candidate_ttys > /dev/null
elapsed=$((SECONDS - start_secs))
if (( elapsed <= 2 )); then
echo " ✓ depth-limited walk terminates promptly"
PASSED=$((PASSED + 1))
else
echo " ✗ depth-limited walk took too long (${elapsed}s)"
FAILED=$((FAILED + 1))
fi
unset -f ps

echo ""
echo "=== Results: $PASSED passed, $FAILED failed ==="

if (( FAILED > 0 )); then
exit 1
fi