Skip to content

Commit c43c850

Browse files
garrytanclaude
andauthored
v1.55.1.0 fix: telemetry consent accuracy + gstack-slug cache sanitization (#1848)
* 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>
1 parent 3bef43b commit c43c850

59 files changed

Lines changed: 356 additions & 108 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
# Changelog
22

3+
## [1.55.1.0] - 2026-06-02
4+
5+
## **Telemetry now tells you exactly what it records and where it stays. The project-slug helper hands the shell a safe identifier on every path.**
6+
7+
The telemetry opt-in screen now states the truth without asterisks: it shares skill name, duration, crashes, and a stable device ID, with no code and no file paths, and your repo name is recorded locally only and stripped before any upload. Under the hood, the helper that every skill uses to find your project (`gstack-slug`) now filters its output to `[a-zA-Z0-9._-]` on every path, including the cached one, so the value that gets handed to the shell is always a plain identifier. Two regression tests lock both behaviors so they can't quietly drift back.
8+
9+
### The guarantees that matter
10+
11+
These are enforced by tests in this release, not promises (`bun test test/telemetry-repo-strip.test.ts test/gstack-slug-sanitize.test.ts`):
12+
13+
| Guarantee | Pinned by |
14+
|-----------|-----------|
15+
| Your repo name never leaves the machine (stripped before upload) | `telemetry-repo-strip.test.ts` — floor + producer-coverage + runs the real strip over a sample event |
16+
| A tampered slug cache can't put shell characters into the helper's output | `gstack-slug-sanitize.test.ts` — fails if the sanitization is removed |
17+
| The consent copy matches what the code actually does | `generate-telemetry-prompt.ts` (regenerated into every skill) |
18+
19+
The repo-identity test covers all three producer fields (`repo`, `_repo_slug`, `_branch`), so adding a new field that forgets to get stripped fails CI rather than shipping silently.
20+
21+
### What this means for you
22+
23+
Your telemetry choice screen now describes what actually happens, so you can opt in (or not) on accurate information. If you share a machine or have ever worried about a tampered `~/.gstack` cache, the slug helper now refuses to pass anything but a safe identifier to the shell. Nothing to do — both land automatically on upgrade.
24+
25+
### Itemized changes
26+
27+
#### Changed
28+
- Telemetry consent copy is now accurate: "No code or file paths. Your repo name is recorded locally only and stripped before any upload" (was "No code, file paths, or repo names").
29+
30+
#### Fixed
31+
- `gstack-slug` sanitizes its output to `[a-zA-Z0-9._-]` on every path, including values read from its on-disk cache, so `eval "$(gstack-slug)"` always receives a plain identifier. A tampered cache file is also healed on the next write.
32+
- The telemetry preamble sanitizes the repo basename before building its JSON line, so an unusual repo directory name can't malform the local analytics record.
33+
34+
#### Added
35+
- `test/telemetry-repo-strip.test.ts` — enforces that no repo/branch identity field reaches the upload batch (floor + producer-coverage + real-strip behavior).
36+
- `test/gstack-slug-sanitize.test.ts` — regression test proving a poisoned slug cache cannot inject shell metacharacters.
37+
38+
#### For contributors
39+
- The consent copy and repo-basename handling live in `scripts/resolvers/preamble/`; all `SKILL.md` files and the ship goldens were regenerated from those resolvers.
40+
341
## [1.55.0.0] - 2026-05-30
442

543
## **`/sync-gbrain` can no longer be the trigger that lets gbrain delete your repo. The headed browser stops crash-looping, and gbrain installs the current release instead of a pin 23 versions stale.**

SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6060
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6161
mkdir -p ~/.gstack/analytics
6262
if [ "$_TEL" != "off" ]; then
63-
echo '{"skill":"gstack","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
63+
echo '{"skill":"gstack","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6464
fi
6565
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
6666
if [ -f "$_PF" ]; then
@@ -170,7 +170,7 @@ Only run `open` if yes. Always run `touch`.
170170

171171
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
172172

173-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
173+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
174174
175175
Options:
176176
- A) Help gstack get better! (recommended)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.55.0.0
1+
1.55.1.0

autoplan/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6969
echo "QUESTION_TUNING: $_QUESTION_TUNING"
7070
mkdir -p ~/.gstack/analytics
7171
if [ "$_TEL" != "off" ]; then
72-
echo '{"skill":"autoplan","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
72+
echo '{"skill":"autoplan","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
7373
fi
7474
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
7575
if [ -f "$_PF" ]; then
@@ -179,7 +179,7 @@ Only run `open` if yes. Always run `touch`.
179179

180180
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
181181

182-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
182+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
183183
184184
Options:
185185
- A) Help gstack get better! (recommended)

benchmark-models/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6363
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6464
mkdir -p ~/.gstack/analytics
6565
if [ "$_TEL" != "off" ]; then
66-
echo '{"skill":"benchmark-models","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
66+
echo '{"skill":"benchmark-models","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6767
fi
6868
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
6969
if [ -f "$_PF" ]; then
@@ -173,7 +173,7 @@ Only run `open` if yes. Always run `touch`.
173173

174174
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
175175

176-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
176+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
177177
178178
Options:
179179
- A) Help gstack get better! (recommended)

benchmark/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6363
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6464
mkdir -p ~/.gstack/analytics
6565
if [ "$_TEL" != "off" ]; then
66-
echo '{"skill":"benchmark","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
66+
echo '{"skill":"benchmark","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6767
fi
6868
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
6969
if [ -f "$_PF" ]; then
@@ -173,7 +173,7 @@ Only run `open` if yes. Always run `touch`.
173173

174174
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
175175

176-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
176+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
177177
178178
Options:
179179
- A) Help gstack get better! (recommended)

bin/gstack-slug

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ fi
3131
# 3. Fallback to basename only when there's truly no git remote configured
3232
SLUG="${SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}"
3333

34+
# 3b. Re-sanitize unconditionally before the value is echoed into `eval`/`source`
35+
# output. The compute (2) and fallback (3) paths already filter, but a value
36+
# read straight from the cache file (1) does NOT — a poisoned
37+
# ~/.gstack/slug-cache/<key> would otherwise inject shell into
38+
# `eval "$(gstack-slug)"`. Filtering here honors the [a-zA-Z0-9._-] invariant
39+
# promised in the header on every path, and heals a poisoned cache on write (4).
40+
SLUG=$(printf '%s' "$SLUG" | tr -cd 'a-zA-Z0-9._-')
41+
3442
# 4. Cache the slug for future sessions (atomic write, fail silently)
3543
if [[ -n "$SLUG" ]]; then
3644
mkdir -p "$CACHE_DIR" 2>/dev/null || true

browse/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6161
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6262
mkdir -p ~/.gstack/analytics
6363
if [ "$_TEL" != "off" ]; then
64-
echo '{"skill":"browse","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
64+
echo '{"skill":"browse","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6565
fi
6666
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
6767
if [ -f "$_PF" ]; then
@@ -171,7 +171,7 @@ Only run `open` if yes. Always run `touch`.
171171

172172
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
173173

174-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
174+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
175175
176176
Options:
177177
- A) Help gstack get better! (recommended)

canary/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6161
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6262
mkdir -p ~/.gstack/analytics
6363
if [ "$_TEL" != "off" ]; then
64-
echo '{"skill":"canary","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
64+
echo '{"skill":"canary","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6565
fi
6666
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
6767
if [ -f "$_PF" ]; then
@@ -171,7 +171,7 @@ Only run `open` if yes. Always run `touch`.
171171

172172
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
173173

174-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
174+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
175175
176176
Options:
177177
- A) Help gstack get better! (recommended)

codex/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ _QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning
6464
echo "QUESTION_TUNING: $_QUESTION_TUNING"
6565
mkdir -p ~/.gstack/analytics
6666
if [ "$_TEL" != "off" ]; then
67-
echo '{"skill":"codex","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
67+
echo '{"skill":"codex","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
6868
fi
6969
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
7070
if [ -f "$_PF" ]; then
@@ -174,7 +174,7 @@ Only run `open` if yes. Always run `touch`.
174174

175175
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
176176

177-
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names.
177+
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
178178
179179
Options:
180180
- A) Help gstack get better! (recommended)

0 commit comments

Comments
 (0)