mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 18:32:19 +02:00
fix(freeze): hook reads the same state root /freeze writes — fails closed under GSTACK_HOME (#1459, #1509)
check-freeze.sh resolved its state dir as ${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}
while every writer (/freeze, /guard, /unfreeze, /investigate) resolves through
bin/gstack-paths, GSTACK_HOME first. With GSTACK_HOME set, /freeze wrote
freeze-dir.txt under GSTACK_HOME, the hook read $HOME/.gstack, found no file,
and allowed everything — a deny-tier boundary failing open.
One resolver now: gstack_hook_state_root() in careful/bin/hook-extract.sh
(already sourced by both check-freeze.sh and check-careful.sh) implements the
exact gstack-paths chain, including the CLAUDE_PLUGIN_ROOT guard that keeps a
CLAUDE_PLUGIN_DATA leaked from another plugin from redirecting our state.
check-freeze.sh and gstack_hook_log_fire both call it; nothing spawns
gstack-paths from a hook.
Tests: the GSTACK_HOME deny regression, GSTACK_HOME-over-CLAUDE_PLUGIN_DATA
precedence, plugin-root guard both ways, and a byte-parity check against
bin/gstack-paths across six env combinations. Existing freeze tests now pass
CLAUDE_PLUGIN_ROOT like a real plugin install would.
Idea from PR #1509 (@NikhileshNanduri); implemented natively against the shared
resolver rather than a second fallback chain.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0d1bd5616c
commit
17733302cc
@@ -63,12 +63,35 @@ gstack_hook_decision() {
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":%s}}\n' "$_ghd_decision" "$_ghd_encoded"
|
||||
}
|
||||
|
||||
# gstack_hook_state_root
|
||||
# Print the gstack state root, resolved with EXACTLY the chain bin/gstack-paths
|
||||
# uses (GSTACK_STATE_ROOT): GSTACK_HOME, then CLAUDE_PLUGIN_DATA only when
|
||||
# CLAUDE_PLUGIN_ROOT names gstack (a CLAUDE_PLUGIN_DATA leaked from another
|
||||
# plugin via CLAUDE_ENV_FILE must not redirect our state), then $HOME/.gstack,
|
||||
# then a project-local .gstack. Hooks run on every Edit/Bash call, so this is
|
||||
# pure bash — never spawn gstack-paths from a hook. The writers (/freeze,
|
||||
# /guard, /unfreeze, /investigate) resolve through gstack-paths; a reader that
|
||||
# used a different chain failed OPEN whenever GSTACK_HOME was set (#1459).
|
||||
# test/hook-scripts.test.ts pins parity against gstack-paths.
|
||||
gstack_hook_state_root() {
|
||||
if [ -n "${GSTACK_HOME:-}" ]; then
|
||||
printf '%s\n' "$GSTACK_HOME"
|
||||
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && printf '%s' "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
|
||||
printf '%s\n' "$CLAUDE_PLUGIN_DATA"
|
||||
elif [ -n "${HOME:-}" ]; then
|
||||
printf '%s\n' "$HOME/.gstack"
|
||||
else
|
||||
printf '%s\n' ".gstack"
|
||||
fi
|
||||
}
|
||||
|
||||
# gstack_hook_log_fire SKILL PATTERN
|
||||
# Append a hook_fire analytics record (pattern name only, never command
|
||||
# content). Respects GSTACK_HOME so tests never pollute the operator's real
|
||||
# analytics file. Best-effort: failures never affect the hook decision.
|
||||
# content). Resolves the state root through gstack_hook_state_root so tests
|
||||
# never pollute the operator's real analytics file. Best-effort: failures
|
||||
# never affect the hook decision.
|
||||
gstack_hook_log_fire() {
|
||||
_ghlf_dir="${GSTACK_HOME:-$HOME/.gstack}/analytics"
|
||||
_ghlf_dir="$(gstack_hook_state_root)/analytics"
|
||||
mkdir -p "$_ghlf_dir" 2>/dev/null || true
|
||||
# Fields are JSON-encoded (a repo basename can carry quotes/backslashes) —
|
||||
# same rule this file states for decisions: never raw-interpolate into JSON.
|
||||
|
||||
Reference in New Issue
Block a user