#!/usr/bin/env bash # # toknmtr-capture.sh — Claude Code hook that triggers an incremental toknmtr agent sweep. # # Registered (by ops/install-hook.sh) under the Claude Code 'Stop' hook event, so it fires # at the end of every assistant turn — see ops/install-hook.sh for the Stop-vs-SessionEnd # tradeoff writeup. # # FAIL-OPEN CONTRACT — this script must NEVER block, slow down, or fail the Claude Code # session it's attached to: # - The actual agent run (`node agent/run.ts --once`) is launched with `nohup ... &` and # disowned, with stdio redirected to a log file (never inherited from the hook's own # stdio), so this script returns to Claude Code in a handful of milliseconds — it does # NOT wait for the parse+push to finish. # - `timeout` wraps the backgrounded agent run so a hung/unreachable server can never # leave an orphaned process running forever. # - Every prerequisite (project dir, config file, node, timeout) is individually checked; # any miss just skips capture for this turn silently. # - This script prints NOTHING to its own stdout/stderr (Claude Code parses Stop-hook # stdout as potential JSON — e.g. {"decision":"block"} would force the session to keep # going — so silence here is required, not just polite). All diagnostic output goes to # $LOG_FILE instead. # - Always exits 0, intentionally not using `set -e`: every step below is already # individually guarded, so a failure anywhere means "skip capture this turn", never # "fail the hook" / block the Stop event. # # Config: ~/.toknmtr/env (TOKNMTR_URL, TOKNMTR_TOKEN — see ops/README.md). Kept out of # ~/.claude/settings.json so secrets aren't sitting in a file that's more likely to be # shared, synced, or dumped for support/debugging. # # Override knobs (env, all optional): # TOKNMTR_PROJECT_DIR path to the toknmtr repo checkout (default: ~/claude/projects/toknmtr) # TOKNMTR_ENV_FILE path to the config file (default: ~/.toknmtr/env) # TOKNMTR_LOG_FILE where backgrounded output is logged (default: ~/.toknmtr/capture.log) # TOKNMTR_HOOK_TIMEOUT_S max seconds the backgrounded sweep may run (default: 25) PROJECT_DIR="${TOKNMTR_PROJECT_DIR:-$HOME/claude/projects/toknmtr}" CONFIG_FILE="${TOKNMTR_ENV_FILE:-$HOME/.toknmtr/env}" LOG_FILE="${TOKNMTR_LOG_FILE:-$HOME/.toknmtr/capture.log}" TIMEOUT_S="${TOKNMTR_HOOK_TIMEOUT_S:-25}" # Claude Code hooks receive a JSON payload on stdin describing the event. We don't need its # contents (the agent re-walks transcripts itself from disk), but drain it anyway so we # never leave the pipe half-read. cat >/dev/null 2>&1 || true # Bail out quietly (still exit 0) if any prerequisite is missing — never surface a hook # failure to the session over a merely-unconfigured machine. [ -d "$PROJECT_DIR" ] || exit 0 [ -f "$PROJECT_DIR/agent/run.ts" ] || exit 0 [ -f "$CONFIG_FILE" ] || exit 0 command -v node >/dev/null 2>&1 || exit 0 mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || exit 0 # Build the backgrounded command as a single string for `bash -c` so it can `cd`, source # the config file (exporting TOKNMTR_URL/TOKNMTR_TOKEN into its own environment), and then # exec node — all inside the detached child, never the foreground hook process. read -r -d '' INNER_CMD </dev/null 2>&1; then nohup timeout "${TIMEOUT_S}s" bash -c "$INNER_CMD" >>"$LOG_FILE" 2>&1 & else nohup bash -c "$INNER_CMD" >>"$LOG_FILE" 2>&1 & fi disown 2>/dev/null || true exit 0