Files
gstack/plan-ceo-review/bin/mode-handoff-hook.ts
Garry Tanandcapy-ai[bot] 4c6fa89cdc v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1) (#3312)
* security: isolate every gstack Bun entry point from the audited checkout's .env and bunfig.toml

Bun auto-loads .env, .env.local and .env.<NODE_ENV> from its cwd and runs a
cwd bunfig.toml preload before the program starts. gstack runs inside the repo
it audits, so a committed .env could steer state roots, the claude binary or
spend caps, and a bunfig preload ran arbitrary code in every bin and hook.

Every bun shebang, shell and skill caller, TypeScript spawn and compiled build
now disables both (--no-env-file --config=/dev/null, bunIsolationArgs(),
--no-compile-autoload-dotenv/bunfig), with a ratchet and hostile-fixture
regressions.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: name the directly executed scripts in the isolation ratchet

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: follow the isolation flags in touchfiles, copy lists, recipe assertions and the harness pin

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: keep browse/src/bun-child-config.ts unchanged so the native cookie qualification still binds

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): the empty bunfig outlives its process so the timeline hook's detached repair child can read it

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: plan-mode render pattern accepts the isolation flags

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-20 00:24:28 +00:00

35 lines
2.0 KiB
TypeScript
Executable File

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* PostToolUse (Bash) hook declared in /plan-ceo-review's frontmatter.
*
* Step 0E's mode handoff line (from bin/gstack-ceo-mode-handoff) must reach
* the user, but Claude Code collapses Bash output, and models often open the
* next chat with a paraphrase instead of the line (periodic red 37272185151;
* 1 of 10 trials led with it even with a reminder beside the line). This hook
* shows the helper's line to the user as a system message, so the user sees
* it whatever the model writes next.
*
* Reads only the helper's own output: the command must be a plain invocation
* of gstack-ceo-mode-handoff and the first stdout line must have the helper's
* exact shape. Anything else prints nothing. Always exits 0.
*/
const HANDOFF_LINE = /^(?:Auto-decided review mode → (?:HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION) \(your preference\)\. Change with \/plan-tune\. Approved decisions: .+\.|Mode: (?:HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION); approved decisions: .+\.)$/;
export function handoffMessage(input: unknown): string | null {
if (!input || typeof input !== 'object') return null;
const { tool_name: tool, tool_input: toolInput, tool_response: response } = input as Record<string, any>;
if (tool !== 'Bash' || typeof toolInput?.command !== 'string') return null;
if (!/(?:^|[\s"'/])gstack-ceo-mode-handoff["']?\s/.test(toolInput.command)) return null;
const stdout: string = typeof response === 'string' ? response : typeof response?.stdout === 'string' ? response.stdout : '';
const line = stdout.split(/\r?\n/).find((l: string) => l.trim())?.trim() ?? '';
return HANDOFF_LINE.test(line) ? line : null;
}
if (import.meta.main) {
try {
const message = handoffMessage(JSON.parse(await Bun.stdin.text()));
if (message) process.stdout.write(JSON.stringify({ systemMessage: message }) + '\n');
} catch { /* A hook failure never blocks the session. */ }
process.exit(0);
}