Files
gstack/bin/gstack-paths
T
Garry TanandClaude Fable 5 7a8e39d2cb fix(mktemp): move the X-run to the end of every temp-file template (BSD/busybox safe)
BSD mktemp (macOS) does not substitute an X-run that has a suffix after it:
`mktemp "$TMP_ROOT/codex-err-XXXXXX.txt"` creates a LITERAL
codex-err-XXXXXX.txt on the first call (exit 0) and every later call fails
with `mkstemp failed: File exists` — so /codex breaks from the SECOND run on
every Mac, masquerading as a model stall. busybox mktemp (Alpine) rejects the
template on the first run. Fixes #2091, #2370.

Union of both community fixes, compared at the diff level:
- PR #2372: all 11 source sites with a suffix after the X-run — codex
  SKILL.md.tmpl (5), claude SKILL.md.tmpl (3), bin/gstack-developer-profile
  (2, suffix folded into the prefix: .json.tmp.XXXXXX), and the office-hours
  codex pass in scripts/resolvers/review.ts (1).
- PR #2103: the second half of #2091 — bin/gstack-paths now strips the
  trailing slash from TMP_ROOT at the source (macOS $TMPDIR ends in `/`),
  plus runtime tests pinning that normalization.

New repo-wide tripwire in test/regression-issue2091-bsd-mktemp.test.ts:
every .tmpl, every SKILL.md, and every scripts/resolvers/*.ts is swept —
no mktemp template may carry a suffix after the X-run, with a self-test so
the detector can't be quietly blinded. Generated SKILL.md files regenerated
via gen:skill-docs in this commit.

Contributed by @ShuratCode (PR #2103) and @noron12234 (PR #2372); PR #2285 by @cathrynlavery covered a subset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 20:20:54 -07:00

75 lines
3.1 KiB
Bash
Executable File

#!/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
# Or: gstack-paths → prints GSTACK_STATE_ROOT=... etc.
#
# 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 (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $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
# State root: where gstack writes projects/, sessions/, analytics/.
if [ -n "${GSTACK_HOME:-}" ]; then
_state_root="$GSTACK_HOME"
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
# be picked up, writing all gstack state into the wrong directory.
_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
# Strip any trailing slash so consumers can safely concatenate "$TMP_ROOT/name"
# without producing a double slash. On macOS $TMPDIR ends in `/` by default
# (e.g. /var/folders/.../T/), which would otherwise yield paths like
# `…/T//codex-err-…`. Normalizing at the source means every consumer benefits,
# not just /codex.
_tmp_root="${_tmp_root%/}"
# A value of "/" collapses to "" above; restore it so TMP_ROOT is never empty.
[ -z "$_tmp_root" ] && _tmp_root="/"
# 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
echo "GSTACK_STATE_ROOT=$_state_root"
echo "PLAN_ROOT=$_plan_root"
echo "TMP_ROOT=$_tmp_root"