#!/usr/bin/env bash # gstack-paths — output portable state-root paths for skill bash blocks # Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT # gstack-paths → prints GSTACK_STATE_ROOT=... etc. # gstack-paths --get → prints a single resolved path (no KEY= prefix) # where is state-root | plan-root | tmp-root # # The --get form was added so callers can use `PLAN_ROOT=$(gstack-paths --get plan-root)` # instead of `eval "$(gstack-paths)"`. The eval form trips Claude Code's PreToolUse # bash AST parser (tilde inside $() inside double-quoted eval arg = "Unhandled node # type: string"), forcing manual approval on every skill that needs portable roots. # See issue #1329 Pattern 2. # # Resolves three roots with explicit fallback chains so skills work the same # whether installed as a Claude Code plugin (CLAUDE_PLUGIN_DATA / CLAUDE_PLANS_DIR # set), a global ~/.claude/skills/gstack/ install, or a local checkout under # CI / container env where HOME may be unset. # # Chains: # GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA -> $HOME/.gstack -> .gstack # PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans # TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort) # # Security: output values are not sanitized — callers may receive paths with # shell-special characters if env vars contain them. Skills should always quote # expansions ("$GSTACK_STATE_ROOT", not $GSTACK_STATE_ROOT). set -u # Parse optional --get selector. _get_key="" case "${1:-}" in --help|-h) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; --get) if [ -z "${2:-}" ]; then echo "gstack-paths: --get requires a key (state-root|plan-root|tmp-root)" >&2 exit 1 fi _get_key="$2" ;; "" ) ;; # bare invocation — backward-compatible default output *) echo "gstack-paths: unknown argument '$1' (expected --get )" >&2 exit 1 ;; esac # State root: where gstack writes projects/, sessions/, analytics/. if [ -n "${GSTACK_HOME:-}" ]; then _state_root="$GSTACK_HOME" elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then _state_root="$CLAUDE_PLUGIN_DATA" elif [ -n "${HOME:-}" ]; then _state_root="$HOME/.gstack" else _state_root=".gstack" fi # Plan root: where /context-save and /codex consult write plan files. if [ -n "${GSTACK_PLAN_DIR:-}" ]; then _plan_root="$GSTACK_PLAN_DIR" elif [ -n "${CLAUDE_PLANS_DIR:-}" ]; then _plan_root="$CLAUDE_PLANS_DIR" elif [ -n "${HOME:-}" ]; then _plan_root="$HOME/.claude/plans" else _plan_root=".claude/plans" fi # Tmp root: where ephemeral files (codex stderr captures, etc.) live. # Honor TMPDIR / TMP for Windows + container compat; fall back to a # project-local .gstack/tmp so we never write to a system /tmp that may # be read-only or shared. if [ -n "${TMPDIR:-}" ]; then _tmp_root="$TMPDIR" elif [ -n "${TMP:-}" ]; then _tmp_root="$TMP" else _tmp_root=".gstack/tmp" fi # Best-effort mkdir; if it fails (read-only fs, permission denied), the caller # will discover that on their own write attempt. Don't fail the eval here. mkdir -p "$_tmp_root" 2>/dev/null || true if [ -n "$_get_key" ]; then case "$_get_key" in state-root) echo "$_state_root" ;; plan-root) echo "$_plan_root" ;; tmp-root) echo "$_tmp_root" ;; *) echo "gstack-paths: unknown key '$_get_key' (expected state-root|plan-root|tmp-root)" >&2 exit 1 ;; esac else echo "GSTACK_STATE_ROOT=$_state_root" echo "PLAN_ROOT=$_plan_root" echo "TMP_ROOT=$_tmp_root" fi