From 201e46b23029d0c552ec8f392f738c4c0f520a5c Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 23:46:35 -0700 Subject: [PATCH] fix: adversarial review fixes (Claude + Codex cross-model passes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both adversarial passes ran against the wave; every FIXABLE finding landed with a regression test: - probeTimeoutMs clamps to >=1ms: a fractional override floored to 0, and execFileSync treats timeout:0 as NO timeout — the probe that exists to bound hangs could hang forever (found by both models independently). - /ship silent hook install now requires the hooks dir to live inside .git: with core.hooksPath (husky's COMMITTED .husky/), the chaining installer would have renamed the team's committed pre-push and written a machine-local wrapper into the working tree (found by both models). - gstack-config gbrain-refresh accepts the "timeout" status — the last consumer still gating on literal "ok" (Codex); gstack-gbrain-detect's config-derived fields honor GBRAIN_HOME so the detection JSON can't report status ok alongside config_exists false (Codex). - prepush: a remote sha absent locally (shallow clone / stale fetch) falls back to the merge-base/empty-tree range — scans MORE, never blocks a legitimate push into training users toward --no-verify. - dashboards: curl's own 000 no longer doubles to "HTTP 000000"; the community dashboard flags stale snapshots like the security one; array sections parse via jq (the sed/grep loops truncated at the first ']'); the no-jq marker grep tolerates whitespace. - telemetry: multi-line redactor output nulls the field instead of corrupting the JSONL record; setup's hint fires only when the config key is genuinely unset (an explicit false is a recorded decline); the /ship prompt marker honors GSTACK_HOME. Kept as designed (cross-model tension noted): Bearer stays MEDIUM in the prepush gate — a HIGH Bearer would block every docs example; the entropy validator can't eliminate that FP class, and MEDIUM warns visibly. Co-Authored-By: Claude Fable 5 --- bin/gstack-community-dashboard | 18 +++++++++--- bin/gstack-config | 6 ++-- bin/gstack-gbrain-detect | 8 ++++- bin/gstack-redact-prepush | 13 +++++++++ bin/gstack-security-dashboard | 34 +++++++++++----------- bin/gstack-telemetry-log | 4 ++- lib/gbrain-local-status.ts | 5 +++- setup | 7 ++++- ship/SKILL.md | 22 +++++++++++--- ship/SKILL.md.tmpl | 22 +++++++++++--- test/fixtures/golden/claude-ship-SKILL.md | 22 +++++++++++--- test/fixtures/golden/codex-ship-SKILL.md | 22 +++++++++++--- test/fixtures/golden/factory-ship-SKILL.md | 22 +++++++++++--- test/gbrain-detect-shape.test.ts | 7 +++-- test/gbrain-local-status.test.ts | 5 ++++ test/redact-prepush-hook.test.ts | 13 +++++++++ test/security-dashboard-fallback.test.ts | 17 ++++++++++- 17 files changed, 197 insertions(+), 50 deletions(-) diff --git a/bin/gstack-community-dashboard b/bin/gstack-community-dashboard index 51ddad579..adb94cb33 100755 --- a/bin/gstack-community-dashboard +++ b/bin/gstack-community-dashboard @@ -38,7 +38,11 @@ trap 'rm -f "$TMPBODY"' EXIT HTTP_CODE="$(curl -s --max-time 15 -w '%{http_code}' -o "$TMPBODY" \ "${SUPABASE_URL}/functions/v1/community-pulse" \ -H "apikey: ${ANON_KEY}" \ - 2>/dev/null || echo "000")" + 2>/dev/null || true)" +# curl prints its own 000 before a non-zero exit — a `|| echo` here would +# double it to "000000" in user-facing output. Normalize to the last 3 chars. +HTTP_CODE="$(printf '%s' "$HTTP_CODE" | tr -d '[:space:]' | tail -c 3)" +[ -n "$HTTP_CODE" ] || HTTP_CODE="000" DATA="$(cat "$TMPBODY" 2>/dev/null || echo "")" echo "gstack community dashboard" @@ -57,15 +61,21 @@ WEEKLY="$(echo "$DATA" | grep -o '"weekly_active":[0-9]*' | grep -o '[0-9]*' || CHANGE="$(echo "$DATA" | grep -o '"change_pct":[0-9-]*' | grep -o '[0-9-]*' || echo "0")" echo "Weekly active installs: ${WEEKLY}" -# Marker check: jq when available (whitespace/reserialization-proof); grep -# fallback only matches the compact form the edge function emits today. +# Marker check: jq when available (whitespace/reserialization-proof); the +# grep fallback tolerates optional whitespace around the colon. +_STALE="false" if command -v jq >/dev/null 2>&1; then _MARKER="$(printf '%s' "$DATA" | jq -r '.status // empty' 2>/dev/null)" + _STALE="$(printf '%s' "$DATA" | jq -r '.stale // false' 2>/dev/null)" else - _MARKER="$(printf '%s' "$DATA" | grep -q '"status":"ok"' && echo ok || true)" + _MARKER="$(printf '%s' "$DATA" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"ok"' && echo ok || true)" fi if [ "$_MARKER" != "ok" ]; then echo " (unverified — legacy backend response; deploy the latest community-pulse for verified figures)" +elif [ "$_STALE" = "true" ]; then + # Backend serves its last good snapshot when recompute fails — real but + # frozen figures must not read as current (matches security-dashboard). + echo " (stale snapshot — backend recompute failing; figures may be out of date)" fi if [ "$CHANGE" -gt 0 ] 2>/dev/null; then echo " Change: +${CHANGE}%" diff --git a/bin/gstack-config b/bin/gstack-config index 5442183b3..d9834c447 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -411,8 +411,10 @@ case "${1:-}" in fi case "$STATUS" in - ok) - echo "Detected gbrain v$VERSION." + ok|timeout) + # "timeout" = slow-but-healthy engine (#1964) — same treatment as + # "ok", matching gstack-gbrain-detect --is-ok and gen-skill-docs. + echo "Detected gbrain v$VERSION (local-status: $STATUS)." # Render brain-aware blocks INTO the global install so EVERY project's # Claude sessions get them (other projects read SKILL.md + sections from # ~/.claude/skills/gstack via absolute paths baked at gen time). Guards diff --git a/bin/gstack-gbrain-detect b/bin/gstack-gbrain-detect index 54f0377c3..ad2380df2 100755 --- a/bin/gstack-gbrain-detect +++ b/bin/gstack-gbrain-detect @@ -48,7 +48,13 @@ import { isTransactionModePooler } from "../lib/gbrain-exec"; const STATE_DIR = process.env.GSTACK_HOME || join(userHome(), ".gstack"); const SCRIPT_DIR = __dirname; const CONFIG_BIN = join(SCRIPT_DIR, "gstack-config"); -const GBRAIN_CONFIG = join(userHome(), ".gbrain", "config.json"); +// Honors GBRAIN_HOME — must stay consistent with lib/gbrain-local-status's +// config resolution, or the detect JSON reports gbrain_local_status "ok" +// alongside gbrain_config_exists false for relocated-home users. +const GBRAIN_CONFIG = join( + process.env.GBRAIN_HOME || join(userHome(), ".gbrain"), + "config.json", +); const CLAUDE_JSON = join(userHome(), ".claude.json"); function userHome(): string { diff --git a/bin/gstack-redact-prepush b/bin/gstack-redact-prepush index 61e242bdf..fd1b05fb9 100755 --- a/bin/gstack-redact-prepush +++ b/bin/gstack-redact-prepush @@ -64,6 +64,12 @@ function gitStrict(args: string[]): string { return r.stdout ?? ""; } +/** True when the object exists in the local odb (cat-file -e signals via exit code). */ +function objectExists(sha: string): boolean { + const r = spawnSync("git", ["cat-file", "-e", sha], { encoding: "utf8" }); + return r.status === 0; +} + function defaultRemoteBranch(): string { // origin/HEAD → origin/main, fall back to main/master. const sym = git(["symbolic-ref", "refs/remotes/origin/HEAD"]).trim(); @@ -83,6 +89,13 @@ function addedLinesFor(localSha: string, remoteSha: string): string { // branch content is scanned as added — fail-safe (scans more, never less). const base = git(["merge-base", localSha, defaultRemoteBranch()]).trim(); range = base ? `${base}..${localSha}` : `${EMPTY_TREE}..${localSha}`; + } else if (!objectExists(remoteSha)) { + // Remote tip object absent locally (shallow clone, force-push without a + // prior fetch, CI checkout): remote..local can't resolve. Fall back to + // the merge-base/empty-tree path — scans MORE, never less — instead of + // hard-blocking a legitimate push (adversarial review finding 8). + const base = git(["merge-base", localSha, defaultRemoteBranch()]).trim(); + range = base ? `${base}..${localSha}` : `${EMPTY_TREE}..${localSha}`; } else { // Existing branch (incl. force-push): net new content remote..local. range = `${remoteSha}..${localSha}`; diff --git a/bin/gstack-security-dashboard b/bin/gstack-security-dashboard index 17bb9199e..9cf524e37 100755 --- a/bin/gstack-security-dashboard +++ b/bin/gstack-security-dashboard @@ -49,7 +49,11 @@ trap 'rm -f "$TMPBODY"' EXIT HTTP_CODE="$(curl -s --max-time 15 -w '%{http_code}' -o "$TMPBODY" \ "${SUPABASE_URL}/functions/v1/community-pulse" \ -H "apikey: ${ANON_KEY}" \ - 2>/dev/null || echo "000")" + 2>/dev/null || true)" +# curl prints its own 000 before a non-zero exit — a `|| echo` here would +# double it to "000000" in user-facing output. Normalize to the last 3 chars. +HTTP_CODE="$(printf '%s' "$HTTP_CODE" | tr -d '[:space:]' | tail -c 3)" +[ -n "$HTTP_CODE" ] || HTTP_CODE="000" DATA="$(cat "$TMPBODY" 2>/dev/null || echo "")" # Classify the response: @@ -119,40 +123,36 @@ elif [ "$TOTAL" = "0" ]; then fi echo "" -# Top attacked domains — parse objects inside top_attack_domains array -DOMAINS="$(echo "$DATA" | sed -n 's/.*"top_attack_domains":\(\[[^]]*\]\).*/\1/p' | head -1)" -if [ -n "$DOMAINS" ] && [ "$DOMAINS" != "[]" ]; then +# Array sections — jq is guaranteed past the state gate; the old sed/grep +# parsing truncated at the first ']' and dropped entries on any nesting +# (the same bug class as the "every count is 7" TOTAL grep). +DOMAINS="$(echo "$DATA" | jq -r '.security.top_attack_domains[]? | "\(.domain)\t\(.count)"' 2>/dev/null)" +if [ -n "$DOMAINS" ]; then echo "Top attacked domains" echo "────────────────────" - echo "$DOMAINS" | grep -o '{[^}]*}' | head -10 | while read -r OBJ; do - DOMAIN="$(echo "$OBJ" | grep -o '"domain":"[^"]*"' | awk -F'"' '{print $4}')" - COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + printf '%s\n' "$DOMAINS" | head -10 | while IFS="$(printf '\t')" read -r DOMAIN COUNT; do [ -n "$DOMAIN" ] && [ -n "$COUNT" ] && printf " %-40s %s attempts\n" "$DOMAIN" "$COUNT" done echo "" fi # Which layer catches attacks -LAYERS="$(echo "$DATA" | sed -n 's/.*"top_attack_layers":\(\[[^]]*\]\).*/\1/p' | head -1)" -if [ -n "$LAYERS" ] && [ "$LAYERS" != "[]" ]; then +LAYERS="$(echo "$DATA" | jq -r '.security.top_attack_layers[]? | "\(.layer)\t\(.count)"' 2>/dev/null)" +if [ -n "$LAYERS" ]; then echo "Top detection layers" echo "────────────────────" - echo "$LAYERS" | grep -o '{[^}]*}' | while read -r OBJ; do - LAYER="$(echo "$OBJ" | grep -o '"layer":"[^"]*"' | awk -F'"' '{print $4}')" - COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + printf '%s\n' "$LAYERS" | while IFS="$(printf '\t')" read -r LAYER COUNT; do [ -n "$LAYER" ] && [ -n "$COUNT" ] && printf " %-28s %s\n" "$LAYER" "$COUNT" done echo "" fi # Verdict distribution -VERDICTS="$(echo "$DATA" | sed -n 's/.*"verdict_distribution":\(\[[^]]*\]\).*/\1/p' | head -1)" -if [ -n "$VERDICTS" ] && [ "$VERDICTS" != "[]" ]; then +VERDICTS="$(echo "$DATA" | jq -r '.security.verdict_distribution[]? | "\(.verdict)\t\(.count)"' 2>/dev/null)" +if [ -n "$VERDICTS" ]; then echo "Verdict distribution" echo "────────────────────" - echo "$VERDICTS" | grep -o '{[^}]*}' | while read -r OBJ; do - VERDICT="$(echo "$OBJ" | grep -o '"verdict":"[^"]*"' | awk -F'"' '{print $4}')" - COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + printf '%s\n' "$VERDICTS" | while IFS="$(printf '\t')" read -r VERDICT COUNT; do [ -n "$VERDICT" ] && [ -n "$COUNT" ] && printf " %-14s %s\n" "$VERDICT" "$COUNT" done echo "" diff --git a/bin/gstack-telemetry-log b/bin/gstack-telemetry-log index 0dd78da57..f94e25462 100755 --- a/bin/gstack-telemetry-log +++ b/bin/gstack-telemetry-log @@ -200,7 +200,9 @@ if (out === null) process.exit(1); console.log(JSON.stringify(out.slice(0, 200))); " 2>/dev/null)" || ERR_MSG_FIELD="null" case "$ERR_MSG_FIELD" in - \"*\") ;; # JSON string — safe to embed + *" +"*) ERR_MSG_FIELD="null" ;; # embedded newline would corrupt the JSONL record + \"*\") ;; # single-line JSON string — safe to embed *) ERR_MSG_FIELD="null" ;; esac fi diff --git a/lib/gbrain-local-status.ts b/lib/gbrain-local-status.ts index 92db47bc4..9e26d5ba4 100644 --- a/lib/gbrain-local-status.ts +++ b/lib/gbrain-local-status.ts @@ -92,7 +92,10 @@ export function probeTimeoutMs(env?: NodeJS.ProcessEnv): number { if (!raw) return DEFAULT_PROBE_TIMEOUT_MS; const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_PROBE_TIMEOUT_MS; - return Math.floor(parsed); + // Floor of 1ms: Math.floor(0.5) would yield 0, and execFileSync treats + // timeout: 0 as NO timeout — the probe that exists to bound hangs would + // itself hang forever (adversarial review finding 2). + return Math.max(1, Math.floor(parsed)); } /** Effective user home — respects HOME env override (used by tests). */ diff --git a/setup b/setup index bd4161c3c..5376c1252 100755 --- a/setup +++ b/setup @@ -1493,7 +1493,12 @@ fi # checkout, the wrong repo to install it into. /ship offers the install once # at the moment of relevance (first push) and silently installs in any repo # where redact_prepush_hook=true. This hint is setup's whole involvement. -if [ "$("$GSTACK_CONFIG" get redact_prepush_hook 2>/dev/null || echo false)" != "true" ]; then +# Hint only when UNSET — an explicit "false" is a recorded decline and must +# not be re-nagged on every setup run (adversarial review finding 11). +# `gstack-config get` defaults absent keys to "false", which is +# indistinguishable from a decline — test key presence in the config file. +_GSTACK_CFG_FILE="${GSTACK_HOME:-$HOME/.gstack}/config.yaml" +if ! grep -q '^redact_prepush_hook:' "$_GSTACK_CFG_FILE" 2>/dev/null; then log "" log "Tip: gstack can block pushes containing credentials (per-repo git hook)." log " Enable once: gstack-config set redact_prepush_hook true — /ship" diff --git a/ship/SKILL.md b/ship/SKILL.md index b7ce93c03..9d6b292dd 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -1225,19 +1225,33 @@ _REDACT_PREPUSH=$(~/.claude/skills/gstack/bin/gstack-config get redact_prepush_h _HOOK_PATH=$(git rev-parse --git-path hooks/pre-push 2>/dev/null || echo "") _HOOK_INSTALLED="no" [ -n "$_HOOK_PATH" ] && [ -f "$_HOOK_PATH" ] && grep -q "gstack-redact" "$_HOOK_PATH" 2>/dev/null && _HOOK_INSTALLED="yes" -_PREPUSH_PROMPTED=$([ -f ~/.gstack/.redact-prepush-prompted ] && echo "yes" || echo "no") +# Custom hooks dirs (core.hooksPath — e.g. husky's COMMITTED .husky/) must +# never get a silent install: the chaining installer would rename the team's +# committed hook and write a machine-local wrapper into the working tree. +_HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") +_GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +_HOOKS_IN_GIT_DIR="no" +case "$_HOOKS_DIR" in + "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; +esac +_PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" echo "HOOK_INSTALLED: $_HOOK_INSTALLED" +echo "HOOKS_IN_GIT_DIR: $_HOOKS_IN_GIT_DIR" echo "PREPUSH_PROMPTED: $_PREPUSH_PROMPTED" ``` Branch on the echoed values: -1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no`** — consent already - given; install silently (no question) and continue: +1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no` and `HOOKS_IN_GIT_DIR: yes`** — + consent already given; install silently (no question) and continue: ```bash ~/.claude/skills/gstack/bin/gstack-redact install-prepush-hook ``` + If `HOOKS_IN_GIT_DIR: no` (husky or another committed hooks dir), do NOT + install silently — print one line: "redact pre-push guard not installed: + this repo uses a custom core.hooksPath; run + `gstack-redact install-prepush-hook` manually if you want it chained." 2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time offer (fires once EVER, machine-wide). AskUserQuestion: @@ -1256,7 +1270,7 @@ Branch on the echoed values: ALWAYS (after either answer, but NOT if the question itself failed to render — a failed AskUserQuestion must re-offer next time): ```bash - touch ~/.gstack/.redact-prepush-prompted + touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ``` 3. **Anything else** (declined earlier, or already installed) — continue without comment. diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 538f154a4..068ac4fe5 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -388,19 +388,33 @@ _REDACT_PREPUSH=$(~/.claude/skills/gstack/bin/gstack-config get redact_prepush_h _HOOK_PATH=$(git rev-parse --git-path hooks/pre-push 2>/dev/null || echo "") _HOOK_INSTALLED="no" [ -n "$_HOOK_PATH" ] && [ -f "$_HOOK_PATH" ] && grep -q "gstack-redact" "$_HOOK_PATH" 2>/dev/null && _HOOK_INSTALLED="yes" -_PREPUSH_PROMPTED=$([ -f ~/.gstack/.redact-prepush-prompted ] && echo "yes" || echo "no") +# Custom hooks dirs (core.hooksPath — e.g. husky's COMMITTED .husky/) must +# never get a silent install: the chaining installer would rename the team's +# committed hook and write a machine-local wrapper into the working tree. +_HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") +_GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +_HOOKS_IN_GIT_DIR="no" +case "$_HOOKS_DIR" in + "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; +esac +_PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" echo "HOOK_INSTALLED: $_HOOK_INSTALLED" +echo "HOOKS_IN_GIT_DIR: $_HOOKS_IN_GIT_DIR" echo "PREPUSH_PROMPTED: $_PREPUSH_PROMPTED" ``` Branch on the echoed values: -1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no`** — consent already - given; install silently (no question) and continue: +1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no` and `HOOKS_IN_GIT_DIR: yes`** — + consent already given; install silently (no question) and continue: ```bash ~/.claude/skills/gstack/bin/gstack-redact install-prepush-hook ``` + If `HOOKS_IN_GIT_DIR: no` (husky or another committed hooks dir), do NOT + install silently — print one line: "redact pre-push guard not installed: + this repo uses a custom core.hooksPath; run + `gstack-redact install-prepush-hook` manually if you want it chained." 2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time offer (fires once EVER, machine-wide). AskUserQuestion: @@ -419,7 +433,7 @@ Branch on the echoed values: ALWAYS (after either answer, but NOT if the question itself failed to render — a failed AskUserQuestion must re-offer next time): ```bash - touch ~/.gstack/.redact-prepush-prompted + touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ``` 3. **Anything else** (declined earlier, or already installed) — continue without comment. diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index b7ce93c03..9d6b292dd 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -1225,19 +1225,33 @@ _REDACT_PREPUSH=$(~/.claude/skills/gstack/bin/gstack-config get redact_prepush_h _HOOK_PATH=$(git rev-parse --git-path hooks/pre-push 2>/dev/null || echo "") _HOOK_INSTALLED="no" [ -n "$_HOOK_PATH" ] && [ -f "$_HOOK_PATH" ] && grep -q "gstack-redact" "$_HOOK_PATH" 2>/dev/null && _HOOK_INSTALLED="yes" -_PREPUSH_PROMPTED=$([ -f ~/.gstack/.redact-prepush-prompted ] && echo "yes" || echo "no") +# Custom hooks dirs (core.hooksPath — e.g. husky's COMMITTED .husky/) must +# never get a silent install: the chaining installer would rename the team's +# committed hook and write a machine-local wrapper into the working tree. +_HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") +_GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +_HOOKS_IN_GIT_DIR="no" +case "$_HOOKS_DIR" in + "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; +esac +_PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" echo "HOOK_INSTALLED: $_HOOK_INSTALLED" +echo "HOOKS_IN_GIT_DIR: $_HOOKS_IN_GIT_DIR" echo "PREPUSH_PROMPTED: $_PREPUSH_PROMPTED" ``` Branch on the echoed values: -1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no`** — consent already - given; install silently (no question) and continue: +1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no` and `HOOKS_IN_GIT_DIR: yes`** — + consent already given; install silently (no question) and continue: ```bash ~/.claude/skills/gstack/bin/gstack-redact install-prepush-hook ``` + If `HOOKS_IN_GIT_DIR: no` (husky or another committed hooks dir), do NOT + install silently — print one line: "redact pre-push guard not installed: + this repo uses a custom core.hooksPath; run + `gstack-redact install-prepush-hook` manually if you want it chained." 2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time offer (fires once EVER, machine-wide). AskUserQuestion: @@ -1256,7 +1270,7 @@ Branch on the echoed values: ALWAYS (after either answer, but NOT if the question itself failed to render — a failed AskUserQuestion must re-offer next time): ```bash - touch ~/.gstack/.redact-prepush-prompted + touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ``` 3. **Anything else** (declined earlier, or already installed) — continue without comment. diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index 3883df2c3..8b3fe7b33 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -2392,19 +2392,33 @@ _REDACT_PREPUSH=$($GSTACK_ROOT/bin/gstack-config get redact_prepush_hook 2>/dev/ _HOOK_PATH=$(git rev-parse --git-path hooks/pre-push 2>/dev/null || echo "") _HOOK_INSTALLED="no" [ -n "$_HOOK_PATH" ] && [ -f "$_HOOK_PATH" ] && grep -q "gstack-redact" "$_HOOK_PATH" 2>/dev/null && _HOOK_INSTALLED="yes" -_PREPUSH_PROMPTED=$([ -f ~/.gstack/.redact-prepush-prompted ] && echo "yes" || echo "no") +# Custom hooks dirs (core.hooksPath — e.g. husky's COMMITTED .husky/) must +# never get a silent install: the chaining installer would rename the team's +# committed hook and write a machine-local wrapper into the working tree. +_HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") +_GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +_HOOKS_IN_GIT_DIR="no" +case "$_HOOKS_DIR" in + "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; +esac +_PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" echo "HOOK_INSTALLED: $_HOOK_INSTALLED" +echo "HOOKS_IN_GIT_DIR: $_HOOKS_IN_GIT_DIR" echo "PREPUSH_PROMPTED: $_PREPUSH_PROMPTED" ``` Branch on the echoed values: -1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no`** — consent already - given; install silently (no question) and continue: +1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no` and `HOOKS_IN_GIT_DIR: yes`** — + consent already given; install silently (no question) and continue: ```bash $GSTACK_ROOT/bin/gstack-redact install-prepush-hook ``` + If `HOOKS_IN_GIT_DIR: no` (husky or another committed hooks dir), do NOT + install silently — print one line: "redact pre-push guard not installed: + this repo uses a custom core.hooksPath; run + `gstack-redact install-prepush-hook` manually if you want it chained." 2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time offer (fires once EVER, machine-wide). AskUserQuestion: @@ -2423,7 +2437,7 @@ Branch on the echoed values: ALWAYS (after either answer, but NOT if the question itself failed to render — a failed AskUserQuestion must re-offer next time): ```bash - touch ~/.gstack/.redact-prepush-prompted + touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ``` 3. **Anything else** (declined earlier, or already installed) — continue without comment. diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 7606687f5..3c6f864f7 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -2798,19 +2798,33 @@ _REDACT_PREPUSH=$($GSTACK_ROOT/bin/gstack-config get redact_prepush_hook 2>/dev/ _HOOK_PATH=$(git rev-parse --git-path hooks/pre-push 2>/dev/null || echo "") _HOOK_INSTALLED="no" [ -n "$_HOOK_PATH" ] && [ -f "$_HOOK_PATH" ] && grep -q "gstack-redact" "$_HOOK_PATH" 2>/dev/null && _HOOK_INSTALLED="yes" -_PREPUSH_PROMPTED=$([ -f ~/.gstack/.redact-prepush-prompted ] && echo "yes" || echo "no") +# Custom hooks dirs (core.hooksPath — e.g. husky's COMMITTED .husky/) must +# never get a silent install: the chaining installer would rename the team's +# committed hook and write a machine-local wrapper into the working tree. +_HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") +_GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +_HOOKS_IN_GIT_DIR="no" +case "$_HOOKS_DIR" in + "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; +esac +_PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" echo "HOOK_INSTALLED: $_HOOK_INSTALLED" +echo "HOOKS_IN_GIT_DIR: $_HOOKS_IN_GIT_DIR" echo "PREPUSH_PROMPTED: $_PREPUSH_PROMPTED" ``` Branch on the echoed values: -1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no`** — consent already - given; install silently (no question) and continue: +1. **`REDACT_PREPUSH: true` and `HOOK_INSTALLED: no` and `HOOKS_IN_GIT_DIR: yes`** — + consent already given; install silently (no question) and continue: ```bash $GSTACK_ROOT/bin/gstack-redact install-prepush-hook ``` + If `HOOKS_IN_GIT_DIR: no` (husky or another committed hooks dir), do NOT + install silently — print one line: "redact pre-push guard not installed: + this repo uses a custom core.hooksPath; run + `gstack-redact install-prepush-hook` manually if you want it chained." 2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time offer (fires once EVER, machine-wide). AskUserQuestion: @@ -2829,7 +2843,7 @@ Branch on the echoed values: ALWAYS (after either answer, but NOT if the question itself failed to render — a failed AskUserQuestion must re-offer next time): ```bash - touch ~/.gstack/.redact-prepush-prompted + touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ``` 3. **Anything else** (declined earlier, or already installed) — continue without comment. diff --git a/test/gbrain-detect-shape.test.ts b/test/gbrain-detect-shape.test.ts index e2f67ee07..761b7326b 100644 --- a/test/gbrain-detect-shape.test.ts +++ b/test/gbrain-detect-shape.test.ts @@ -43,7 +43,10 @@ function runDetect(env: Partial): string { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, ...env }, + // GBRAIN_HOME pinned empty: detect honors it (codex D11), and sibling + // test files in the same shard set it ambiently — without the pin, the + // spawned detect reads the polluter's (or the developer's real) config. + env: { ...process.env, GBRAIN_HOME: "", ...env }, }); } @@ -52,7 +55,7 @@ function runIsOk(env: Partial): number { const r = spawnSync(BUN_BIN, ["run", DETECT_BIN, "--is-ok"], { timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, ...env }, + env: { ...process.env, GBRAIN_HOME: "", ...env }, }); return r.status ?? 1; } diff --git a/test/gbrain-local-status.test.ts b/test/gbrain-local-status.test.ts index 46ab9ad88..703adfcad 100644 --- a/test/gbrain-local-status.test.ts +++ b/test/gbrain-local-status.test.ts @@ -297,6 +297,11 @@ describe("probeTimeoutMs — env override parsing", () => { expect(probeTimeoutMs({ GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "0" })).toBe(DEFAULT_PROBE_TIMEOUT_MS); expect(probeTimeoutMs({ GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "-5" })).toBe(DEFAULT_PROBE_TIMEOUT_MS); }); + + it("never returns 0 for fractional sub-millisecond values (0 = NO timeout in execFileSync)", () => { + expect(probeTimeoutMs({ GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "0.5" })).toBe(1); + expect(probeTimeoutMs({ GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "0.0001" })).toBe(1); + }); }); describe("lib/gbrain-local-status — cache behavior", () => { diff --git a/test/redact-prepush-hook.test.ts b/test/redact-prepush-hook.test.ts index fa38ff0db..cf8598523 100644 --- a/test/redact-prepush-hook.test.ts +++ b/test/redact-prepush-hook.test.ts @@ -128,6 +128,19 @@ describe("fail closed on unscannable diffs (#1946)", () => { expect(code).toBe(0); }); + test("a remote sha absent locally (shallow clone / stale fetch) falls back to scanning MORE, not blocking", () => { + // Adversarial review finding 8: remote..local can't resolve when the + // remote tip object isn't in the local odb. The fallback scans the + // merge-base/empty-tree range — a secret in the pushed content still + // blocks; a clean push passes instead of hard-failing. + const fakeRemoteSha = "c".repeat(40); + const head = commit("secrets.txt", "key AKIA1234567890ABCDEF\n", "leaky commit"); + const { code, stderr } = runHook(`refs/heads/main ${head} refs/heads/main ${fakeRemoteSha}\n`); + expect(code).toBe(1); // fallback range still catches the credential + expect(stderr).toContain("aws.access_key"); + expect(stderr).not.toContain("could not compute the pushed diff"); + }); + test("a diff killed by a signal (null status — the maxBuffer/kill class) BLOCKS", () => { // Stub git: probes delegate to the real git; the diff invocation kills // itself, producing spawnSync status === null. This is the exact branch diff --git a/test/security-dashboard-fallback.test.ts b/test/security-dashboard-fallback.test.ts index d9612725b..3c07ab4de 100644 --- a/test/security-dashboard-fallback.test.ts +++ b/test/security-dashboard-fallback.test.ts @@ -101,7 +101,7 @@ function run( // needs — everything except jq. const toolBin = join(tmp, "tool-bin"); mkdirSync(toolBin, { recursive: true }); - for (const tool of ["mktemp", "cat", "grep", "head", "sed", "awk", "rm", "sh", "bash"]) { + for (const tool of ["mktemp", "cat", "grep", "head", "sed", "awk", "rm", "sh", "bash", "tr", "tail"]) { const real = Bun.which(tool); if (real) symlinkSync(real, join(toolBin, tool)); } @@ -254,4 +254,19 @@ describe("gstack-community-dashboard — never reports fake zeros (#1947)", () = expect(r.stdout).toContain("Weekly active installs: 42"); expect(r.stdout).not.toContain("unverified"); }); + + it("stale snapshot flagged in human mode (matches security-dashboard)", () => { + const staleBody = JSON.stringify({ ...JSON.parse(GOOD_BODY_MARKER), stale: true }); + const r = run(COMM_BIN, { mode: "ok", body: staleBody }); + expect(r.stdout).toContain("Weekly active installs: 42"); + expect(r.stdout).toContain("stale snapshot"); + }); + + it("network failure reports HTTP 000, never a doubled 000000", () => { + // Adversarial review finding 6: curl prints its own 000 before a + // non-zero exit; a `|| echo` doubled it in user-facing output. + const r = run(COMM_BIN, { mode: "netfail" }); + expect(r.stdout).toContain("(HTTP 000)"); + expect(r.stdout).not.toContain("000000"); + }); });