mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-04 17:18:11 +02:00
c43c850cae
* fix(gstack-slug): sanitize cached slug before eval The compute and fallback paths filter slug output to [a-zA-Z0-9._-], but a value read straight from ~/.gstack/slug-cache was echoed into eval output unsanitized. A locally-planted cache file could inject shell into eval "$(gstack-slug)". Re-sanitize on every path so the invariant the file header promises actually holds, and heal a poisoned cache on the next write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(telemetry): accurate consent copy + JSON-safe repo basename The telemetry consent prompt promised "no repo names" while the preamble epilogue records the repo basename in the local skill-usage.jsonl. It is already stripped before any remote upload, so it never left the machine, but the copy was unqualified. Reword it to state repo name is local-only and stripped before upload. Also sanitize the basename to [a-zA-Z0-9._-] before it goes into the hand-built JSON, so a repo directory name containing quotes or newlines can neither break the JSON nor leak a fragment past the regex stripper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(docs): regenerate SKILL.md + ship goldens for telemetry change Generated output of the preceding resolver change: the corrected consent copy and sanitized repo basename now appear in every skill preamble. Golden ship fixtures refreshed to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(telemetry): enforce no-repo-identity-egress invariant Pins the contract that repo/branch identity in the synced skill-usage.jsonl is stripped before the remote POST. Three checks: a floor (the three known fields), coverage (every repo/branch field a producer writes into skill-usage.jsonl is stripped, so a future producer rename can't silently leak), and behavior (runs the actual sed strip expressions over a sample event). Scoped to the synced file, so the local-only timeline branch field is correctly excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(gstack-slug): regression test for cached-slug eval injection Proves a poisoned ~/.gstack/slug-cache file cannot inject shell metacharacters into gstack-slug output (the value consumed by eval). Verified red when the cache-read sanitization is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.55.1.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2.4 KiB
Bash
Executable File
56 lines
2.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-slug — output project slug and sanitized branch name
|
|
# Usage: eval "$(gstack-slug)" → sets SLUG and BRANCH variables
|
|
# Or: gstack-slug → prints SLUG=... and BRANCH=... lines
|
|
#
|
|
# Security: output is sanitized to [a-zA-Z0-9._-] only, preventing
|
|
# shell injection when consumed via source or eval.
|
|
set -euo pipefail
|
|
|
|
CACHE_DIR="$HOME/.gstack/slug-cache"
|
|
PROJECT_DIR="$(pwd)"
|
|
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo
|
|
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_')
|
|
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"
|
|
|
|
# 1. Try cached slug first (guarantees consistency across sessions)
|
|
if [[ -f "$CACHE_FILE" ]]; then
|
|
SLUG=$(cat "$CACHE_FILE")
|
|
fi
|
|
|
|
# 2. If no cache, compute from git remote (separated from pipeline to avoid
|
|
# pipefail swallowing the error and producing an empty slug)
|
|
if [[ -z "${SLUG:-}" ]]; then
|
|
REMOTE_URL=$(git remote get-url origin 2>/dev/null) || REMOTE_URL=""
|
|
if [[ -n "$REMOTE_URL" ]]; then
|
|
RAW_SLUG=$(printf '%s' "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
|
|
SLUG=$(printf '%s' "$RAW_SLUG" | tr -cd 'a-zA-Z0-9._-')
|
|
fi
|
|
fi
|
|
|
|
# 3. Fallback to basename only when there's truly no git remote configured
|
|
SLUG="${SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}"
|
|
|
|
# 3b. Re-sanitize unconditionally before the value is echoed into `eval`/`source`
|
|
# output. The compute (2) and fallback (3) paths already filter, but a value
|
|
# read straight from the cache file (1) does NOT — a poisoned
|
|
# ~/.gstack/slug-cache/<key> would otherwise inject shell into
|
|
# `eval "$(gstack-slug)"`. Filtering here honors the [a-zA-Z0-9._-] invariant
|
|
# promised in the header on every path, and heals a poisoned cache on write (4).
|
|
SLUG=$(printf '%s' "$SLUG" | tr -cd 'a-zA-Z0-9._-')
|
|
|
|
# 4. Cache the slug for future sessions (atomic write, fail silently)
|
|
if [[ -n "$SLUG" ]]; then
|
|
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
CACHE_TMP=$(mktemp "$CACHE_DIR/.slug-XXXXXX" 2>/dev/null) || CACHE_TMP=""
|
|
if [[ -n "$CACHE_TMP" ]]; then
|
|
printf '%s' "$SLUG" > "$CACHE_TMP" && mv "$CACHE_TMP" "$CACHE_FILE" 2>/dev/null || rm -f "$CACHE_TMP" 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
RAW_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || RAW_BRANCH=""
|
|
BRANCH=$(printf '%s' "${RAW_BRANCH:-}" | tr -cd 'a-zA-Z0-9._-')
|
|
BRANCH="${BRANCH:-unknown}"
|
|
echo "SLUG=$SLUG"
|
|
echo "BRANCH=$BRANCH"
|