`gsd-sdk query check.ship-ready <phase>` built a git command as a shell
string with the current branch name interpolated. Git branch names can
legally contain shell metacharacters, so a repo checked out on a
malicious branch like `foo;touch${IFS}INJ;bar` executed arbitrary shell
commands.
Vulnerability site (pre-fix):
sdk/src/query/check-ship-ready.ts:50
runSyncSafe(`git config --get branch.${current_branch}.merge`, cwd)
→ execSync('git config --get branch.foo;touch${IFS}INJ;bar.merge')
→ /bin/sh -c parses three commands; the middle one runs `touch INJ`
in the project dir and creates the sentinel file.
Manually reproduced on git 2.53.0:
- refname `foo;touch${IFS}INJ;bar` is accepted by `git check-ref-format`
and by `git checkout -b`.
- `current_branch` returned from `git rev-parse --abbrev-ref HEAD`
contains the metacharacters verbatim.
- Interpolation into the buggy execSync call creates the sentinel.
Fix:
- Replace `runSyncSafe(cmd: string, cwd)` (execSync, shell-string) with
`runArgvSafe(file, args: readonly string[], cwd)` (execFileSync,
argv-based, no shell).
- Same shape for the boolean wrapper: `boolArgvSafe`.
- Convert all 7 subprocess sites in the module to argv form:
- `git status --porcelain`
- `git rev-parse --abbrev-ref HEAD`
- `git config --get branch.<name>.merge` ← the interpolation site
- `git rev-parse --verify main`
- `git remote`
- `gh --version`
- `which gh`
- Shell is never invoked. Branch names — even ones with `;`, `$IFS`,
backticks, `$()` — are passed as a single argv element and treated
as opaque data.
Regression test (`sdk/src/query/check-ship-ready.test.ts`):
- `#3587: branch name with shell-injection payload does not execute
injected command` — creates a real git repo, checks out the proven
exploit branch `foo;touch${IFS}INJECTED_BY_3587;bar`, runs
checkShipReady, and asserts the sentinel file does NOT exist. This
test FAILS on the unfixed code (verified pre-implementation) and
PASSES on the fixed code — true red→green TDD.
- `#3587: round-trips a metacharacter branch name verbatim in
current_branch` — positive proof the branch name survives argv as
data (would fail if a future change re-introduces shell quoting).
- `#3587: gh probe does not invoke a shell` — locks the gh path
against a future regression that might add an interpolation site.
Validation:
- SDK unit suite via vitest: 1,869/1,869 pass.
- Full root suite via gsd-test-both (per CLAUDE.md): 10,676/10,676
on Mac AND 10,676/10,676 on Linux Docker, zero cross-platform diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gsd-build/sdk
TypeScript SDK for Get Shit Done: deterministic query/mutation handlers, plan execution, and event-stream telemetry so agents focus on judgment, not shell plumbing.
Install
npm install @gsd-build/sdk
Quickstart — programmatic
import { GSD, createRegistry } from '@gsd-build/sdk';
const gsd = new GSD({ projectDir: process.cwd(), sessionId: 'my-run' });
const tools = gsd.createTools();
const registry = createRegistry(gsd.eventStream, 'my-run');
const { data } = await registry.dispatch('state.json', [], process.cwd());
Quickstart — CLI
From a project that depends on this package, invoke the CLI with Node (recommended in CI and local dev):
node ./node_modules/@gsd-build/sdk/dist/cli.js query state.json
node ./node_modules/@gsd-build/sdk/dist/cli.js query roadmap.analyze
If no native handler is registered for a command, the CLI can transparently shell out to get-shit-done/bin/gsd-tools.cjs (see stderr warning), unless GSD_QUERY_FALLBACK=off.
What ships
| Area | Entry |
|---|---|
| Query registry | createRegistry() in src/query/index.ts — same handlers as gsd-sdk query |
| Tools bridge | GSDTools — native dispatch with optional CJS subprocess fallback |
| Orchestrators | PhaseRunner, InitRunner, GSD |
| CLI | gsd-sdk — query, run, init, auto |
Guides
- Handler registry & contracts:
src/query/QUERY-HANDLERS.md - Repository docs (when present):
docs/ARCHITECTURE.md,docs/CLI-TOOLS.mdat repo root
Environment
| Variable | Purpose |
|---|---|
GSD_QUERY_FALLBACK |
off / never disables CLI fallback to gsd-tools.cjs for unknown commands |
GSD_AGENTS_DIR |
Override directory scanned for installed GSD agents (~/.claude/agents by default) |