fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell#1964
fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell#1964aznikline wants to merge 2 commits into
Conversation
|
Independently took a run at this before I noticed #1964 — and landed on the same core fix (quote-aware tokenizer → Two bits from my version that might be worth folding into #1964, if useful:
Also minor: #1964 targets Full write-up on my fork if it's worth a skim — PRD → design → ADR: PRD · design · ADR. Happy to send the docs + test as a follow-up to #1964 rather than open a competing PR. Disclosure (per contribution rules): produced with an autonomous development harness — gitban (muunkky.github.io/gitban-site) — on Claude Code 2.1.207 / Claude Opus 4.8; reasoned from the issue + direct code inspection. |
…regression test Fold in two bits from muunkky's parallel run on obra#1957 (offered as a follow-up, not a competing PR): - visual-companion.md: a BRAINSTORM_OPEN_CMD section — accepted shapes (bare command, command+args, quoted argv), trust posture (env var = user authority), and the no-shell note (the launcher tokenizes and runs without a shell, so the semicolon/ampersand in the value are literal, not interpreted). The issue title mentions docs and obra#1964 was code-only. - lifecycle.test.js: a direct shell-injection test — BRAINSTORM_OPEN_CMD set to a capture command plus a touch of a pwned flag asserts the pwned flag is never created, proving metacharacters can't splice a second command. Complements the existing URL-integrity assertion (which proves the URL query chars stay intact).
2c4b301 to
0e8622f
Compare
|
@muunkky — really nice writeup, and a strong signal to see two independent passes converge on the same tokenizer → I folded both of your bits straight into #1964 (appreciate you offering them as a follow-up rather than competing):
Also retargeted the PR base @obra — ready for review when you have a moment. Closes #1957. |
Two fresh-context audits re-executed every command and re-checked every claim in the skill against live GitHub. The evidence base is mostly sound — every quotation but one, every cited PR state, the medians, the batch forensics all survive. But it was breaking its own first rule in six places, and one of them was a fabricated signal we would have acted on. THE FOOTER WAS NEVER A SIGNAL. The skill said obra's 'closures can be revisited' footer appears on only 16 of 42 closures, never on a fault-track one, so its ABSENCE meant he thought you cheated. It is on all 42 — including obra#1906 (fabricated reviewer), obra#1925 (empty file) and obra#1166 (byte-identical dupe). Root cause, and the whole lesson: the original string-check in obra-triage-analysis.md grepped for a MISQUOTED footer ('If any of the evidence above is wrong' — he actually writes 'If you think this call is wrong'). The misquote matched an arbitrary subset, and that subset was rationalized into a track theory with an exception list. One unverified quotation became a confident, actionable, fictional rule. Both the report and the skill are corrected. jq IS NOT INSTALLED. I prescribed 'jq -r .enabledPlugins' yesterday as THE command for the plugin disclosure — the control guarding the sole-sufficient kill. It exits 127. Replaced with python3 (which preflight already used) and selftest now fails if the skill prescribes a tool the machine lacks. The RED/GREEN eval rig could not run as printed: 'tar -x -C A/' with no mkdir. The rig that unblocks every skills PR, and nobody had run it. '43% of closures carry no comment at all' is a true number with a false label: 304/714 is 'obra never commented'. Literally-silent is 114 (16%). The triage would recompute that in one query. Also: 'main tracks upstream/dev' was false and contradicted the sync section I fixed an hour ago; obra#1964 'landed' when it is still open; the obra#1957 comment was actually on obra#1964; 'slop, 22 hits in the corpus' is unsourced (0 in our own report); baseline refreshed and date-stamped (84.2%, 134 merges — the 42 genuinely-external figure is unchanged). And WATCH.state was tracked, because my own 'git add -f <dir>' forced it past the skill's .gitignore. Every sweep was dirtying the tree. selftest: 37 green.
…bra#1957) Replace cp.exec(cmd + url) with cp.execFile over a quote-aware tokenized argv, matching the platform launchers' no-shell pattern. Adds shellArgv() helper and a regression test asserting the URL arrives as a single argv element.
…regression test Fold in two bits from muunkky's parallel run on obra#1957 (offered as a follow-up, not a competing PR): - visual-companion.md: a BRAINSTORM_OPEN_CMD section — accepted shapes (bare command, command+args, quoted argv), trust posture (env var = user authority), and the no-shell note (the launcher tokenizes and runs without a shell, so the semicolon/ampersand in the value are literal, not interpreted). The issue title mentions docs and obra#1964 was code-only. - lifecycle.test.js: a direct shell-injection test — BRAINSTORM_OPEN_CMD set to a capture command plus a touch of a pwned flag asserts the pwned flag is never created, proving metacharacters can't splice a second command. Complements the existing URL-integrity assertion (which proves the URL query chars stay intact).
0e8622f to
b04f985
Compare
…regression test Fold in two bits from muunkky's parallel run on obra#1957 (offered as a follow-up, not a competing PR): - visual-companion.md: a BRAINSTORM_OPEN_CMD section — accepted shapes (bare command, command+args, quoted argv), trust posture (env var = user authority), and the no-shell note (the launcher tokenizes and runs without a shell, so the semicolon/ampersand in the value are literal, not interpreted). The issue title mentions docs and obra#1964 was code-only. - lifecycle.test.js: a direct shell-injection test — BRAINSTORM_OPEN_CMD set to a capture command plus a touch of a pwned flag asserts the pwned flag is never created, proving metacharacters can't splice a second command. Complements the existing URL-integrity assertion (which proves the URL query chars stay intact).
…regression test Fold in two bits from muunkky's parallel run on obra#1957 (offered as a follow-up, not a competing PR): - visual-companion.md: a BRAINSTORM_OPEN_CMD section — accepted shapes (bare command, command+args, quoted argv), trust posture (env var = user authority), and the no-shell note (the launcher tokenizes and runs without a shell, so the semicolon/ampersand in the value are literal, not interpreted). The issue title mentions docs and obra#1964 was code-only. - lifecycle.test.js: a direct shell-injection test — BRAINSTORM_OPEN_CMD set to a capture command plus a touch of a pwned flag asserts the pwned flag is never created, proving metacharacters can't splice a second command. Complements the existing URL-integrity assertion (which proves the URL query chars stay intact).
What
Hardens the
BRAINSTORM_OPEN_CMDbrowser-launch path inskills/brainstorming/scripts/server.cjsso it no longer goes through a shell. Closes #1957.Why
The security audit in #1957 flagged that
maybeOpenBrowser()ran the operator-providedBRAINSTORM_OPEN_CMDviacp.exec(cmd + ' ' + JSON.stringify(url)), i.e. throughsh -c. The env var is opt-in, loopback-only, trusted operator input, so the practical risk is bounded — but the shell-exec shape is what the audit (and downstream org security reviews) block adoption on. The issue's proposed resolution was to match the platform launchers two lines below, which already useexecFilewith the URL as a single argv element.How
cp.execbranch withcp.execFile(bin, args.concat(url), …)— no shell, the URL is a single argv element.shellArgv()that splits the operator's command string into an argv honoring single/double quotes (so a command likenode "/path/with space/capture.cjs" "/marker/path"still resolves to the right tokens, matching how the existing test fixture buildsBRAINSTORM_OPEN_CMD).nulland is rejected — it is never handed to a shell as a fallback. The previoustry/catchbest-effort semantics are preserved on the actual spawn.This mirrors the existing
browserLauncherForPlatformpattern (bin + args +execFile), so the two launcher paths now share the same no-shell shape.Tests
lifecycle.test.jstests forBRAINSTORM_OPEN_CMDstill pass — their fixture (node "script" "marker") tokenizes cleanly to[node, script, marker], with the URL appended as the final element.BRAINSTORM_OPEN_CMDlaunches without a shell and keeps the URL as one argv element. It captures the launchedargvand asserts the server URL (which carries a?key=query) arrives as exactly one token with no shell-splitting — the direct proof the injection shape is gone.Behavior change
Operators who set
BRAINSTORM_OPEN_CMDto a command relying on shell features (pipes,&&, env expansion, globbing) will need to wrap it in a script — the command is now tokenized andexecFile'd, not shell-interpreted. This matches how the platform launchers already behave and is the intended hardening. I called this out in the env var's comment.Checklist