Files
loop-loop/lib/hooks.sh
Sheldon Finlay bab002b927 fix: prevent Stop hook from killing sessions outside tmux
tmux display-message succeeds even outside tmux by falling back to the
most recently created session (agent-loop). This caused the hook to
match and kill interactive CC sessions.

Fix: check $TMUX env var first — only set when actually inside tmux.
2026-04-02 09:14:43 -04:00

64 lines
2.6 KiB
Bash

#!/bin/bash
# Stop hook management for the agent loop.
#
# The Stop hook sends SIGINT to the loop's parent process when a claude session
# finishes. This is how claude signals "I'm done" back to the bash loop —
# the Ralph pattern (ghuntley.com/ralph).
#
# Without this hook, claude would exit to an interactive prompt instead of
# returning control to the loop script.
#
# IMPORTANT: The hook is scoped to only fire inside the agent-loop tmux session.
# Without this guard, ANY Claude Code session opened in the same project directory
# would pick up the hook and kill its own parent shell on exit.
SETTINGS_FILE="${PROJECT_ROOT}/.claude/settings.local.json"
# The hook command checks we're inside the agent-loop tmux session before killing.
# This prevents the hook from affecting other CC sessions open in the same project.
# IMPORTANT: Must check $TMUX first — tmux display-message can succeed even outside
# tmux by falling back to the most recently created session, which would be agent-loop.
HOOK_COMMAND='[ -n "$TMUX" ] && tmux display-message -p "#{session_name}" 2>/dev/null | grep -q "^agent-loop$" && kill -INT $PPID || true'
install_hooks() {
if [ ! -f "$SETTINGS_FILE" ]; then
mkdir -p "$(dirname "$SETTINGS_FILE")"
echo '{}' > "$SETTINGS_FILE"
fi
if command -v jq &>/dev/null; then
jq --arg cmd "$HOOK_COMMAND" \
'.hooks.Stop = [{"matcher": "", "hooks": [{"type": "command", "command": $cmd}]}]' \
"$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
else
LOOP_HOOK_CMD="$HOOK_COMMAND" LOOP_SETTINGS="$SETTINGS_FILE" python3 -c "
import json, os
p = os.environ['LOOP_SETTINGS']
cmd = os.environ['LOOP_HOOK_CMD']
s = json.load(open(p)) if os.path.exists(p) else {}
s.setdefault('hooks', {})['Stop'] = [{'matcher': '', 'hooks': [{'type': 'command', 'command': cmd}]}]
json.dump(s, open(p, 'w'), indent=2)
"
fi
log "Stop hook installed"
}
remove_hooks() {
if [ -f "$SETTINGS_FILE" ]; then
if command -v jq &>/dev/null; then
jq 'del(.hooks.Stop)' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
jq 'if .hooks == {} then del(.hooks) else . end' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
else
LOOP_SETTINGS="$SETTINGS_FILE" python3 -c "
import json, os
p = os.environ['LOOP_SETTINGS']
s = json.load(open(p))
s.get('hooks', {}).pop('Stop', None)
if not s.get('hooks'): s.pop('hooks', None)
json.dump(s, open(p, 'w'), indent=2)
"
fi
log "Stop hook removed"
fi
}