#!/usr/bin/env bash # Stop hook: tear down the ai-term session (or power off) AFTER a wrap-up. # # wrap-it-up.org drops a sentinel only at the very end of a teardown- or # shutdown-mode wrap, once commit+push is verified and the valediction is # delivered. This hook fires when Claude stops responding; on every NORMAL # stop there is no sentinel, so it is a silent no-op. Only after a wrap that # requested teardown does the matching sentinel exist, and only then does this # hook act — which is what keeps a routine end-of-turn from killing the # session. # # Decoupling via the Stop hook (rather than an inline workflow step) is what # lets the valediction flush before the session dies: teardown kills the very # tmux session Claude runs in, so it cannot happen inline. # # Two sentinels, keyed by the project basename (one ai-term session per # project, named aiv-): # /tmp/ai-wrap-teardown- -> cj/ai-term-quit "" # kill the aiv- tmux session (takes claude with it), kill the # vterm buffer, restore geometry. Defined in .emacs.d/modules/ai-term.el. # /tmp/ai-wrap-shutdown- -> cj/ai-term-shutdown-countdown # abort-able 10->1 echo-area countdown, then sudo shutdown now. Shutdown # supersedes teardown (killing the buffer is moot if powering off). # # The sentinel is removed BEFORE the emacsclient call: cj/ai-term-quit kills # the tmux session this hook runs inside, so the hook process may not survive # the call. Clearing first guarantees the sentinel never lingers to re-fire on # a later session in the same project. The emacs daemon is a separate process, # so it completes the teardown even when the hook is cut off mid-call. # # emacsclient absent or daemon unreachable: clear the sentinel and exit 0. The # session simply stays up — graceful degradation, never a wedge. # # Wire in ~/.claude/settings.json (see hooks/settings-snippet.json): # # "Stop": [ # { "hooks": [ # { "type": "command", # "command": "~/.claude/hooks/ai-wrap-teardown.sh" } ] } ] set -u # Stop-hook stdin JSON carries cwd; basename it to the project / aiv- session. cwd="$(jq -r '.cwd // empty' 2>/dev/null)" [ -z "$cwd" ] && cwd="$PWD" proj="$(basename "$cwd")" teardown_sentinel="/tmp/ai-wrap-teardown-${proj}" shutdown_sentinel="/tmp/ai-wrap-shutdown-${proj}" fire() { # $1 = elisp form. Best-effort: only when emacsclient resolves. command -v emacsclient >/dev/null 2>&1 || return 0 emacsclient -e "$1" >/dev/null 2>&1 || true } # Shutdown supersedes teardown when both are somehow present. if [ -f "$shutdown_sentinel" ]; then rm -f "$shutdown_sentinel" "$teardown_sentinel" fire '(cj/ai-term-shutdown-countdown)' exit 0 fi if [ -f "$teardown_sentinel" ]; then rm -f "$teardown_sentinel" fire "(cj/ai-term-quit \"${proj}\")" exit 0 fi exit 0