Fix hooks.
This commit is contained in:
+83
-16
@@ -8,16 +8,15 @@
|
||||
# playpen-status.sh idle # finished
|
||||
# playpen-status.sh error # stopped on an error
|
||||
#
|
||||
# The state is written as OSC 9;4 — the ConEmu progress protocol — straight to
|
||||
# /dev/tty, and on `busy` the prompt is written as an OSC 0 title alongside it.
|
||||
# The state is written as OSC 9;4 — the ConEmu progress protocol — to the pty
|
||||
# Claude Code is running on, and on `busy` the prompt is written as an OSC 0
|
||||
# title alongside it.
|
||||
#
|
||||
# Why /dev/tty and not a socket. A hook runs as a child of Claude Code, so its
|
||||
# controlling terminal is the pty of the pane Claude is running in. Writing
|
||||
# there means the bytes arrive in that pane and no other, with nothing to
|
||||
# configure and no way for two concurrent sessions to be confused for each
|
||||
# other. It survives a VM boundary too: run Claude inside a microVM and the
|
||||
# guest's console passes these through to the host pty unchanged, which a unix
|
||||
# socket on the host could not do.
|
||||
# Why a pty and not a socket. The bytes then arrive in that pane and no other,
|
||||
# with nothing to configure and no way for two concurrent sessions to be
|
||||
# confused for each other. It survives a VM boundary too: run Claude inside a
|
||||
# microVM and the guest's console passes these through to the host pty
|
||||
# unchanged, which a unix socket on the host could not do.
|
||||
#
|
||||
# Nothing here is Playpen-specific. OSC 9;4 is what Windows Terminal, ConEmu
|
||||
# and Ghostty already use for taskbar progress, so these hooks light up those
|
||||
@@ -27,10 +26,78 @@ set -u
|
||||
|
||||
state=${1:-idle}
|
||||
|
||||
# No controlling terminal — running headless, in CI, or under a harness that
|
||||
# detached us. Nothing to report to, and a hook must never be the thing that
|
||||
# breaks a session, so leave quietly.
|
||||
[ -w /dev/tty ] || exit 0
|
||||
# ---------------------------------------------------------------------------
|
||||
# Find the pty to write to.
|
||||
#
|
||||
# Not /dev/tty, which is the obvious answer and the wrong one: Claude Code
|
||||
# starts each hook in its own session, so a hook has no controlling terminal
|
||||
# and opening /dev/tty fails with ENXIO. Worse, `[ -w /dev/tty ]` still says
|
||||
# yes — it stats the path, whose mode is 0666, rather than opening it — so
|
||||
# guarding on that reports success and then writes into nothing.
|
||||
#
|
||||
# The pty is still one hop away: it is on Claude Code's own standard fds. So
|
||||
# test our controlling terminal by actually opening it, and otherwise walk up
|
||||
# the process tree for the nearest ancestor holding a pty open. That nearest
|
||||
# ancestor is the right answer even when something else is in between: a
|
||||
# Claude running inside tmux inside a pane finds tmux's pty, which is where
|
||||
# its output is really going.
|
||||
tty_target=""
|
||||
|
||||
if (: > /dev/tty) 2>/dev/null; then
|
||||
tty_target=/dev/tty
|
||||
fi
|
||||
|
||||
if [ -z "$tty_target" ] && [ -d /proc ]; then
|
||||
pid=$PPID
|
||||
hops=0
|
||||
while [ "$hops" -lt 16 ] && [ -n "$pid" ] && [ "$pid" != 0 ] && [ "$pid" != 1 ]; do
|
||||
for fd in 0 1 2; do
|
||||
link=$(readlink "/proc/$pid/fd/$fd" 2>/dev/null) || continue
|
||||
case "$link" in
|
||||
# Guarded to terminal devices before opening it for write, so this
|
||||
# can never truncate a regular file an fd happened to point at.
|
||||
/dev/pts/[0-9]* | /dev/tty[0-9]*)
|
||||
if (: > "$link") 2>/dev/null; then
|
||||
tty_target=$link
|
||||
break
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ -n "$tty_target" ] && break
|
||||
|
||||
# Walk to the parent. The comm field of /proc/pid/stat is parenthesised
|
||||
# and may itself contain spaces, so cut through it rather than counting
|
||||
# fields from the start: after the trim, $2 is the ppid.
|
||||
stat=$(cat "/proc/$pid/stat" 2>/dev/null) || break
|
||||
# shellcheck disable=SC2086
|
||||
set -- ${stat#*") "}
|
||||
pid=${2:-}
|
||||
hops=$((hops + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
# No /proc to walk: ask ps for the parent's terminal instead. Linux spells it
|
||||
# "pts/4" and macOS "s004", so try it both as given and with the tty prefix.
|
||||
if [ -z "$tty_target" ]; then
|
||||
name=$(ps -o tty= -p "$PPID" 2>/dev/null | tr -d ' \n')
|
||||
case "$name" in
|
||||
"" | "?" | "??") ;;
|
||||
*)
|
||||
for cand in "/dev/$name" "/dev/tty$name"; do
|
||||
if (: > "$cand") 2>/dev/null; then
|
||||
tty_target=$cand
|
||||
break
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Nothing anywhere to report to — headless, in CI, or a harness that detached
|
||||
# us from every terminal. A hook must never be the thing that breaks a
|
||||
# session, so leave quietly.
|
||||
[ -n "$tty_target" ] || exit 0
|
||||
|
||||
case "$state" in
|
||||
busy) code=3 ;; # indeterminate
|
||||
@@ -39,10 +106,10 @@ case "$state" in
|
||||
*) code=0 ;; # removed
|
||||
esac
|
||||
|
||||
# Every write goes to /dev/tty explicitly. stdout belongs to Claude Code, which
|
||||
# Every write goes to the pty explicitly. stdout belongs to Claude Code, which
|
||||
# parses it as the hook's JSON result; an escape sequence written there would
|
||||
# corrupt the hook protocol rather than reach the terminal.
|
||||
printf '\033]9;4;%s\007' "$code" > /dev/tty 2>/dev/null || true
|
||||
printf '\033]9;4;%s\007' "$code" > "$tty_target" 2>/dev/null || true
|
||||
|
||||
# Only a starting turn carries a task worth naming. The other states leave the
|
||||
# title alone so the shell's own title comes back when the session ends.
|
||||
@@ -72,6 +139,6 @@ fi
|
||||
# and leave the rest to be printed as garbage in the pane.
|
||||
title=$(printf '%s' "$title" | head -n 1 | tr -d '[:cntrl:]' | cut -c1-72)
|
||||
|
||||
[ -n "$title" ] && printf '\033]0;%s\007' "$title" > /dev/tty 2>/dev/null
|
||||
[ -n "$title" ] && printf '\033]0;%s\007' "$title" > "$tty_target" 2>/dev/null
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user