#!/usr/bin/env bash set -euo pipefail # # ops/install-hook.sh — idempotently register the toknmtr capture hook # (agent/hooks/toknmtr-capture.sh) in ~/.claude/settings.json under the Claude Code 'Stop' # hook event. # # WHY 'Stop' AND NOT 'SessionEnd': # Stop fires at the end of EVERY assistant turn (each time Claude finishes responding and # yields back to the user) — not just once when the whole CLI process exits. That makes # capture close to live: a dashboard refresh a few seconds after a turn already reflects # it. The cost is the hook running more often, but agent/hooks/toknmtr-capture.sh is # designed to be near-zero-cost on the foreground session (it backgrounds the real work # and returns in milliseconds — see that script's header), so the extra frequency is # nearly free. # SessionEnd only fires once, when the CLI process actually exits — on a long interactive # session that can be hours after the data was generated, and Claude Code does not # guarantee SessionEnd fires on every exit path (e.g. a killed/crashed terminal can skip # it entirely), so relying on it alone risks losing the tail of a session permanently # until the next cron sweep (ops/install-cron.sh) catches it. # Net: Stop is the better default given the hook is cheap. The cron sweep is an # independent safety net either way (server-down resilience, missed hook fires, etc.) — # it doesn't change this tradeoff, it just makes the choice lower-stakes. Revisit by # swapping HOOK_EVENT below to "SessionEnd" if Stop's per-turn frequency ever proves to be # a real problem in practice (e.g. very chatty multi-turn sessions on a slow machine). # # What this script does: # - Merges (via jq) a Stop-hook entry pointing at agent/hooks/toknmtr-capture.sh into # ~/.claude/settings.json, WITHOUT touching any other keys/hooks already in that file. # - Idempotent: matches on the exact command string, so re-running is a safe no-op. # - Writes a timestamped backup of settings.json before every modification. # - Reversible: `ops/install-hook.sh --remove` undoes just this hook's entry (or restore # a backup manually — see the "to remove later" line this script prints on install). # # Usage: # ops/install-hook.sh # install/update the hook # ops/install-hook.sh --remove # remove this hook's entry from settings.json # # Override knobs (env, all optional): # TOKNMTR_PROJECT_DIR path to the toknmtr repo checkout (default: ~/claude/projects/toknmtr) # CLAUDE_SETTINGS_FILE path to settings.json (default: ~/.claude/settings.json) PROJECT_DIR="${TOKNMTR_PROJECT_DIR:-$HOME/claude/projects/toknmtr}" SETTINGS_FILE="${CLAUDE_SETTINGS_FILE:-$HOME/.claude/settings.json}" HOOK_SCRIPT="$PROJECT_DIR/agent/hooks/toknmtr-capture.sh" HOOK_EVENT="Stop" HOOK_CMD="bash \"$HOOK_SCRIPT\"" HOOK_TIMEOUT_S=10 command -v jq >/dev/null 2>&1 || { echo "error: jq is required (sudo apt install jq / brew install jq)" >&2 exit 1 } backup_settings() { [ -f "$SETTINGS_FILE" ] || return 0 local stamp stamp="$(date +%Y%m%dT%H%M%S)" cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak.$stamp" echo "backed up $SETTINGS_FILE -> $SETTINGS_FILE.bak.$stamp" } if [ "${1:-}" = "--remove" ]; then if [ ! -f "$SETTINGS_FILE" ]; then echo "no settings file at $SETTINGS_FILE, nothing to remove" exit 0 fi backup_settings tmp="$(mktemp)" jq --arg cmd "$HOOK_CMD" --arg event "$HOOK_EVENT" ' if (.hooks // {} | has($event)) then .hooks[$event] = [ .hooks[$event][] | .hooks = [(.hooks // [])[] | select(.command != $cmd)] | select((.hooks | length) > 0) ] else . end ' "$SETTINGS_FILE" >"$tmp" && mv "$tmp" "$SETTINGS_FILE" echo "removed toknmtr capture hook from $SETTINGS_FILE.$HOOK_EVENT (backup written above; other $HOOK_EVENT hooks, if any, were left untouched)" exit 0 fi if [ ! -f "$HOOK_SCRIPT" ]; then echo "error: $HOOK_SCRIPT not found (is TOKNMTR_PROJECT_DIR=$PROJECT_DIR correct?)" >&2 exit 1 fi chmod +x "$HOOK_SCRIPT" 2>/dev/null || true mkdir -p "$(dirname "$SETTINGS_FILE")" [ -f "$SETTINGS_FILE" ] || echo '{}' >"$SETTINGS_FILE" jq empty "$SETTINGS_FILE" || { echo "error: $SETTINGS_FILE is not valid JSON — fix it manually before running this script" >&2 exit 1 } backup_settings tmp="$(mktemp)" jq --arg cmd "$HOOK_CMD" --arg event "$HOOK_EVENT" --argjson timeout "$HOOK_TIMEOUT_S" ' .hooks //= {} | .hooks[$event] //= [] | ([.hooks[$event][]? | (.hooks // [])[]? | .command] | index($cmd)) as $already | if $already == null then .hooks[$event] += [{"hooks": [{"type": "command", "command": $cmd, "timeout": $timeout}]}] else . end ' "$SETTINGS_FILE" >"$tmp" && mv "$tmp" "$SETTINGS_FILE" echo "installed toknmtr capture hook into $SETTINGS_FILE under hooks.$HOOK_EVENT" echo " command: $HOOK_CMD" echo "" echo "to remove later:" echo " ops/install-hook.sh --remove" echo " (or restore a $SETTINGS_FILE.bak. file written next to it)"