#!/usr/bin/env bash # gstack-hook-check — parse-check every hook Claude Code runs from a gstack tree. # # Usage: # gstack-hook-check [--setup ] --list print the checked hooks # gstack-hook-check [--setup ] check them # # Claude Code runs gstack's hook shims through /bin/sh; a shim that does not # parse exits 2, which for a PreToolUse hook blocks the tool call in every # session. setup runs this before it registers hooks, and the auto-updater # (bin/gstack-session-update) runs it on the incoming revision before the # live checkout moves. # # The hook list comes from the registration code: every # `_hook_command_path ` argument in /setup (or the setup named by # --setup, when another tree's setup registers 's hooks), plus the autoplan # phase-publication hook (registered by its skill frontmatter). # test/setup-hook-syntax-gate.test.ts keeps the two equal. # # Each shim is parsed by the interpreter its shebang names (bash -n, sh -n). # Each TypeScript entry the shim runs (a `.ts` beside it) is bundled with # its local imports into a temp directory, packages external, so a broken or # missing local import fails; a missing entry fails. # # Output, one line per hook: # ok # missing (no shim: setup skips registering it) # fail :: # Exit: 0 when nothing failed, 1 when a hook failed, 2 on usage, 3 when bun # (needed for the TypeScript check) is not found. Set GSTACK_HOOK_CHECK_BUN to # the bun to run (setup passes its own). set -u LIST_ONLY=0 SETUP_FILE="" while [ $# -gt 1 ]; do case "$1" in --list) LIST_ONLY=1; shift ;; --setup) SETUP_FILE="$2"; shift 2 ;; *) break ;; esac done ROOT="${1:-}" if [ -z "$ROOT" ] || [ ! -d "$ROOT" ]; then echo "usage: gstack-hook-check [--setup ] [--list] " >&2 exit 2 fi SETUP_FILE="${SETUP_FILE:-$ROOT/setup}" hook_list() { sed -n 's/.*\$(_hook_command_path \([A-Za-z0-9._/-]*\).*/\1/p' "$SETUP_FILE" 2>/dev/null echo autoplan/bin/phase-publication-hook } HOOKS=$(hook_list | awk '!seen[$0]++') if [ "$LIST_ONLY" -eq 1 ]; then printf '%s\n' "$HOOKS" exit 0 fi BUN="${GSTACK_HOOK_CHECK_BUN:-bun}" if ! command -v "$BUN" >/dev/null 2>&1; then echo "gstack-hook-check: bun not found, so TypeScript hook entries cannot be checked" >&2 exit 3 fi TMP_OUT=$(mktemp -d "${TMPDIR:-/tmp}/gstack-hook-check.XXXXXX") || { echo "gstack-hook-check: cannot create a temp directory" >&2; exit 1; } trap 'rm -rf "$TMP_OUT"' EXIT # first_location — ":" from bun's "at f:l:c" # or bash's "f: line N:" (falls back to ). first_location() { _loc=$(printf '%s\n' "$2" | sed -n 's/^[[:space:]]*at \(.*\):\([0-9][0-9]*\):[0-9][0-9]*$/\1:\2/p' | head -1) [ -n "$_loc" ] || _loc=$(printf '%s\n' "$2" | sed -n 's/^\(.*\): line \([0-9][0-9]*\):.*/\1:\2/p' | head -1) printf '%s\n' "${_loc:-$1}" } first_message() { _msg=$(printf '%s\n' "$1" | sed -n 's/^error: //p' | head -1) [ -n "$_msg" ] || _msg=$(printf '%s\n' "$1" | sed -n 's/^.*: line [0-9][0-9]*: //p' | head -1) [ -n "$_msg" ] || _msg=$(printf '%s\n' "$1" | grep -v '^[[:space:]]*$' | head -1) printf '%s\n' "$_msg" } FAILED=0 fail() { echo "fail $1 $2: $3"; FAILED=1; } for hook in $HOOKS; do shim="$ROOT/$hook" if [ ! -f "$shim" ]; then echo "missing $hook"; continue; fi hook_ok=1 shebang=$(head -1 "$shim") case "$shebang" in '#!'*bash*) interp=bash ;; '#!'*/sh|'#!'*/sh' '*|'#!'*' sh'|'#!'*' sh '*) interp=sh ;; *) interp="" ;; esac if [ -z "$interp" ]; then fail "$hook" "$shim:1" "unsupported shebang '$shebang' (expected bash or sh)" continue fi if ! err=$("$interp" -n "$shim" 2>&1); then fail "$hook" "$(first_location "$shim" "$err")" "$(first_message "$err")" hook_ok=0 fi dir=$(dirname "$shim") for entry in $(grep -v '^[[:space:]]*#' "$shim" | grep -oE '[A-Za-z0-9._-]+\.ts([^A-Za-z0-9_]|$)' | sed -E 's/\.ts.*$/.ts/' | awk '!seen[$0]++'); do if [ ! -f "$dir/$entry" ]; then fail "$hook" "$dir/$entry:1" "missing TypeScript entry (the hook runs it)" hook_ok=0 continue fi if ! err=$("$BUN" build --target=bun --packages=external --outdir "$TMP_OUT/$(basename "$hook")" "$dir/$entry" 2>&1 >/dev/null); then fail "$hook" "$(first_location "$dir/$entry" "$err")" "$(first_message "$err")" hook_ok=0 fi done [ "$hook_ok" -eq 1 ] && echo "ok $hook" done exit "$FAILED"