72 lines
2.4 KiB
Bash
72 lines
2.4 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
# Claude Code Stop hook — fires when the agent finishes responding.
|
|||
|
|
#
|
|||
|
|
# Option D: bash pre-filter first, LLM triage only for ambiguous cases.
|
|||
|
|
#
|
|||
|
|
# Known Stop event fields:
|
|||
|
|
# session_id, transcript_path, cwd, permission_mode,
|
|||
|
|
# hook_event_name ("Stop"), stop_hook_active
|
|||
|
|
#
|
|||
|
|
# Note: stop_reason is NOT provided in the Stop event JSON.
|
|||
|
|
# We detect state by inspecting the tmux pane instead.
|
|||
|
|
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||
|
|
SUPERVISOR_DIR="${SCRIPT_DIR%/hooks}"
|
|||
|
|
|
|||
|
|
INPUT=$(cat)
|
|||
|
|
|
|||
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|||
|
|
CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"')
|
|||
|
|
|
|||
|
|
# Find matching supervised session
|
|||
|
|
STATE_FILE="${CCS_STATE_FILE:-${HOME}/.openclaw/workspace/supervisor-state.json}"
|
|||
|
|
if [ ! -f "$STATE_FILE" ]; then
|
|||
|
|
exit 0 # No supervised sessions
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
SOCKET=$(jq -r --arg cwd "$CWD" '
|
|||
|
|
.sessions | to_entries[] | select(.value.projectDir == $cwd and .value.status == "running") | .value.socket
|
|||
|
|
' "$STATE_FILE" 2>/dev/null || echo "")
|
|||
|
|
|
|||
|
|
TMUX_SESSION=$(jq -r --arg cwd "$CWD" '
|
|||
|
|
.sessions | to_entries[] | select(.value.projectDir == $cwd and .value.status == "running") | .value.tmuxSession
|
|||
|
|
' "$STATE_FILE" 2>/dev/null || echo "")
|
|||
|
|
|
|||
|
|
# No matching supervised session for this project
|
|||
|
|
if [ -z "$SOCKET" ] || [ -z "$TMUX_SESSION" ]; then
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Can we reach the tmux session?
|
|||
|
|
if [ ! -S "$SOCKET" ] || ! tmux -S "$SOCKET" has-session -t "$TMUX_SESSION" 2>/dev/null; then
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
PANE_OUTPUT=$(tmux -S "$SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S -30 2>/dev/null || echo "")
|
|||
|
|
|
|||
|
|
# Bash pre-filter: check the last few lines for patterns
|
|||
|
|
|
|||
|
|
# Check for API errors (transient — always triage)
|
|||
|
|
if echo "$PANE_OUTPUT" | tail -10 | grep -qiE "API Error: 5[0-9][0-9]|internal.server.error|api_error"; then
|
|||
|
|
"$SUPERVISOR_DIR/triage.sh" "stopped:api_error" "$CWD" "$PANE_OUTPUT" &
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Check for rate limiting
|
|||
|
|
if echo "$PANE_OUTPUT" | tail -10 | grep -qiE "429|rate.limit"; then
|
|||
|
|
echo "[$(date -u +%FT%TZ)] RATE_LIMITED | stop | $CWD" >> "${CCS_LOG_FILE:-/tmp/ccs-triage.log}"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Check if shell prompt is back (Claude Code exited)
|
|||
|
|
if echo "$PANE_OUTPUT" | tail -5 | grep -qE '^\$ |^❯ [^/]|^% |^[a-z]+@.*\$ '; then
|
|||
|
|
# Prompt returned — agent might be done or crashed. Triage it.
|
|||
|
|
"$SUPERVISOR_DIR/triage.sh" "stopped:prompt_back" "$CWD" "$PANE_OUTPUT" &
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# No prompt back, no errors — agent is likely mid-conversation. Skip silently.
|
|||
|
|
exit 0
|