fix(hooks): partial upgrades fail closed for freeze and fall back for careful

A hook script and its sourced helper can be copied at different times. With
an older careful/bin/hook-extract.sh that lacks gstack_hook_state_root:

- check-freeze.sh now emits a deny ("fail closed, re-run ./setup or
  /unfreeze") instead of dying under set -e with no decision JSON.
- check-careful.sh falls back to ${GSTACK_HOME:-$HOME/.gstack} so project
  rules under the plain chain still load and a decision is always emitted
  (a warn hook must never break on a stale helper).

gstack_hook_state_root prints its root without a trailing newline and both
callers capture it with a printf-x sentinel, so a GSTACK_HOME ending in a
newline round-trips byte-for-byte with the writer's %q form.
gstack_hook_log_fire stays on ${GSTACK_HOME:-$HOME/.gstack}/analytics, the
same two-step chain every other analytics writer and reader uses, so the
usage log remains one file under a plugin install.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-09-04 17:28:00 +00:00
co-authored by Claude Fable 5.1
parent 29237ff9a8
commit b8d347df35
4 changed files with 161 additions and 10 deletions
+8 -1
View File
@@ -36,7 +36,14 @@ fi
# fails open: with GSTACK_HOME set, /freeze wrote freeze-dir.txt under
# GSTACK_HOME while this hook read $HOME/.gstack, found nothing, and allowed
# everything (#1459, #1509). gstack_hook_state_root mirrors gstack-paths.
STATE_DIR="$(gstack_hook_state_root)"
# A helper from an older install that lacks the function must fail CLOSED
# (the existence check above only proves the file sourced), never exit 127
# with no JSON — Claude Code treats that as non-blocking.
if ! command -v gstack_hook_state_root >/dev/null 2>&1; then
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[freeze] Hook helpers out of date (partial upgrade?) - blocked, fail closed. Re-run ./setup or /unfreeze."}}\n'
exit 0
fi
STATE_DIR="$(gstack_hook_state_root; printf x)"; STATE_DIR="${STATE_DIR%x}"
FREEZE_FILE="$STATE_DIR/freeze-dir.txt"
# If no freeze file exists, allow everything (not yet configured)