mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 07:59:02 +02:00
fix: red-team findings — verify-gate identity, single quoting authority, Windows paths
Red-team pass over the hardened diff (several findings empirically verified by the reviewer before reporting): - KNOWN_HOOKS gains the sixth identity: gstack-verify-gate (README-documented opt-in Stop hook). A tag-stripped verify-gate entry previously survived prune-stale --all and errored at the end of EVERY turn after uninstall deleted the install root — the exact phantom-hook class this branch fixes. Uninstall also sweeps its tagged form. - add-event is now the single quoting authority: every registered command is normalized through the same gsQuoteCmd/gsStripWrap round-trip the healer uses. Pre-fix, only SessionStart got caller-side quoting — a spaced/metachar canonical root registered broken plan-tune/AUQ/timeline hooks that the very next heal rewrote (the codebase disagreed with its own registrations). - Windows: MSYS-form paths (/c/Users/...) are drive-translated for fs checks only (gsWinPath) — native bun resolved them drive-relative, so the heal judged every LIVE Windows hook dead and pruned it. The three AskUserQuestion hooks and the Stop hook now also get the mandatory 'bash ' prefix on Windows (previously only SessionStart did; extensionless bash shims otherwise hit the file-association dialog). - CANONICAL_GSTACK_ROOT falls back to $HOME/.claude/skills/gstack when a CLAUDE_CONFIG_DIR-derived root was never installed (the installer hardcodes the home path — split-brain left such users permanently hookless). - prune-stale preserves foreign entries that STARTED empty (they were silently deleted, uncounted, on every heal). - The timeline Stop registration and its list-sources guard join the zero-silent-mutations contract (stderr attached). Tests: verify-gate tag-stripped heal+sweep, started-empty preservation, add-event quoting-authority round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
19eed1b392
commit
6a3cf611ea
@@ -1870,6 +1870,14 @@ SETTINGS_HOOK="$SOURCE_GSTACK_DIR/bin/gstack-settings-hook"
|
||||
# WARNING for future code AND migrations (the v1.58.0.0.sh defect class):
|
||||
# NEVER register ${SCRIPT_DIR}/$SOURCE_GSTACK_DIR-relative hook paths.
|
||||
CANONICAL_GSTACK_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/gstack"
|
||||
# Split-brain guard: the installer currently hardcodes $HOME/.claude/skills
|
||||
# (setup:1601 TODO), so a CLAUDE_CONFIG_DIR override can name a root that was
|
||||
# never installed. Fall back to where the install actually lives — both are
|
||||
# stable, neither is the running tree, so canonical-only still holds.
|
||||
if [ ! -x "$CANONICAL_GSTACK_ROOT/bin/gstack-session-update" ] \
|
||||
&& [ -x "$HOME/.claude/skills/gstack/bin/gstack-session-update" ]; then
|
||||
CANONICAL_GSTACK_ROOT="$HOME/.claude/skills/gstack"
|
||||
fi
|
||||
|
||||
# Echo the canonical path for a hook (repo-relative arg); fails when the hook
|
||||
# is not executable at the canonical install — callers must skip + log.
|
||||
@@ -1920,14 +1928,9 @@ fi
|
||||
SESSION_UPDATE_CMD="$(_hook_command_path bin/gstack-session-update || true)"
|
||||
HOOK_CMD=""
|
||||
if [ -n "$SESSION_UPDATE_CMD" ]; then
|
||||
# The registered command is executed by a shell when Claude Code fires the
|
||||
# hook — neutralize shell metacharacters, not just whitespace (mirrors
|
||||
# gsQuoteCmd in gstack-settings-hook).
|
||||
case "$SESSION_UPDATE_CMD" in
|
||||
*[\ \"\$\`\\]*)
|
||||
SESSION_UPDATE_CMD="\"$(printf '%s' "$SESSION_UPDATE_CMD" | sed 's/[\\"$`]/\\&/g')\""
|
||||
;;
|
||||
esac
|
||||
# No caller-side quoting: add-event is the single quoting authority — it
|
||||
# normalizes every registered command through the same gsQuoteCmd round-trip
|
||||
# the healer uses, so metachar/space paths cannot drift per call site.
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
HOOK_CMD="bash $SESSION_UPDATE_CMD"
|
||||
else
|
||||
@@ -2073,6 +2076,14 @@ fi
|
||||
PLAN_TUNE_LOG_HOOK="$(_hook_command_path hosts/claude/hooks/question-log-hook || true)"
|
||||
PLAN_TUNE_PREF_HOOK="$(_hook_command_path hosts/claude/hooks/question-preference-hook || true)"
|
||||
AUQ_ERROR_FALLBACK_HOOK="$(_hook_command_path hosts/claude/hooks/auq-error-fallback-hook || true)"
|
||||
# Windows: extensionless bash shims need the explicit 'bash ' prefix (same
|
||||
# rationale as HOOK_CMD above — the OS file-association dialog otherwise).
|
||||
# KNOWN_HOOKS identity round-trips the prefix, so healing preserves it.
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
[ -n "$PLAN_TUNE_LOG_HOOK" ] && PLAN_TUNE_LOG_HOOK="bash $PLAN_TUNE_LOG_HOOK"
|
||||
[ -n "$PLAN_TUNE_PREF_HOOK" ] && PLAN_TUNE_PREF_HOOK="bash $PLAN_TUNE_PREF_HOOK"
|
||||
[ -n "$AUQ_ERROR_FALLBACK_HOOK" ] && AUQ_ERROR_FALLBACK_HOOK="bash $AUQ_ERROR_FALLBACK_HOOK"
|
||||
fi
|
||||
PLAN_TUNE_INSTALL_MARKER="$HOME/.gstack/.plan-tune-hooks-prompted"
|
||||
|
||||
# Canonical-only: an ephemeral tree with no stable install gets a visible skip,
|
||||
@@ -2275,13 +2286,18 @@ fi
|
||||
# a session. Idempotent via the (event, source) dedup in gstack-settings-hook;
|
||||
# removed by --no-team and gstack-uninstall.
|
||||
TIMELINE_STOP_HOOK="$(_hook_command_path hosts/claude/hooks/timeline-stop-hook || true)"
|
||||
if [ "$IS_WINDOWS" -eq 1 ] && [ -n "$TIMELINE_STOP_HOOK" ]; then
|
||||
TIMELINE_STOP_HOOK="bash $TIMELINE_STOP_HOOK"
|
||||
fi
|
||||
# stderr stays attached on both calls below (zero silent settings mutations;
|
||||
# the list-sources corrupt-file warning must reach the user too).
|
||||
if [ "$NO_TEAM_MODE" -ne 1 ] && [ -x "$SETTINGS_HOOK" ] && [ -n "$TIMELINE_STOP_HOOK" ]; then
|
||||
if ! "$SETTINGS_HOOK" list-sources 2>/dev/null | grep -q "gstack-timeline-stop"; then
|
||||
if ! "$SETTINGS_HOOK" list-sources | grep -q "gstack-timeline-stop"; then
|
||||
if "$SETTINGS_HOOK" add-event \
|
||||
--event Stop \
|
||||
--command "$TIMELINE_STOP_HOOK" \
|
||||
--source gstack-timeline-stop \
|
||||
--timeout 5 >/dev/null 2>&1; then
|
||||
--timeout 5 >/dev/null; then
|
||||
log " registered Stop hook: session timeline entries now close even when a skill is interrupted (backup: settings.json.bak.<ts>; remove: $SETTINGS_HOOK remove-source --source gstack-timeline-stop)"
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user