Skip to content

fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell#1964

Open
aznikline wants to merge 2 commits into
obra:devfrom
aznikline:fix/brainstorm-open-cmd-no-shell
Open

fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell#1964
aznikline wants to merge 2 commits into
obra:devfrom
aznikline:fix/brainstorm-open-cmd-no-shell

Conversation

@aznikline

Copy link
Copy Markdown

What

Hardens the BRAINSTORM_OPEN_CMD browser-launch path in skills/brainstorming/scripts/server.cjs so it no longer goes through a shell. Closes #1957.

Why

The security audit in #1957 flagged that maybeOpenBrowser() ran the operator-provided BRAINSTORM_OPEN_CMD via cp.exec(cmd + ' ' + JSON.stringify(url)), i.e. through sh -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 use execFile with the URL as a single argv element.

How

  • Replaced the cp.exec branch with cp.execFile(bin, args.concat(url), …) — no shell, the URL is a single argv element.
  • Added a quote-aware tokenizer shellArgv() that splits the operator's command string into an argv honoring single/double quotes (so a command like node "/path/with space/capture.cjs" "/marker/path" still resolves to the right tokens, matching how the existing test fixture builds BRAINSTORM_OPEN_CMD).
  • A command with unbalanced quotes returns null and is rejected — it is never handed to a shell as a fallback. The previous try/catch best-effort semantics are preserved on the actual spawn.

This mirrors the existing browserLauncherForPlatform pattern (bin + args + execFile), so the two launcher paths now share the same no-shell shape.

Tests

  • Existing lifecycle.test.js tests for BRAINSTORM_OPEN_CMD still pass — their fixture (node "script" "marker") tokenizes cleanly to [node, script, marker], with the URL appended as the final element.
  • Added a regression test: BRAINSTORM_OPEN_CMD launches without a shell and keeps the URL as one argv element. It captures the launched argv and 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_CMD to a command relying on shell features (pipes, &&, env expansion, globbing) will need to wrap it in a script — the command is now tokenized and execFile'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

  • Change is scoped to the flagged line and its helper
  • No new shell surface introduced
  • Existing tests green; added a regression test for the no-shell contract

@muunkky

muunkky commented Jul 11, 2026

Copy link
Copy Markdown

Independently took a run at this before I noticed #1964 — and landed on the same core fix (quote-aware tokenizer → execFile, no shell). Nice to see it converge; good sign it's the right shape.

Two bits from my version that might be worth folding into #1964, if useful:

Also minor: #1964 targets main; but I pushed mine to dev as per contribution rules.

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.

aznikline added a commit to aznikline/superpowers that referenced this pull request Jul 13, 2026
…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).
@aznikline
aznikline force-pushed the fix/brainstorm-open-cmd-no-shell branch from 2c4b301 to 0e8622f Compare July 13, 2026 02:53
@aznikline
aznikline changed the base branch from main to dev July 13, 2026 02:55
@aznikline

Copy link
Copy Markdown
Author

@muunkky — really nice writeup, and a strong signal to see two independent passes converge on the same tokenizer → execFile shape.

I folded both of your bits straight into #1964 (appreciate you offering them as a follow-up rather than competing):

  • Operator docs — added a BRAINSTORM_OPEN_CMD section to visual-companion.md covering the accepted shapes (bare command / command+args / quoted argv), the trust posture (env var = invoking user's authority), and the no-shell note (the launcher tokenizes and runs without a shell, so ;/& in the value are literal). The issue title does call out docs, and fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell #1964 was code-only — good catch.
  • Direct injection test — added alongside the URL-integrity regression: BRAINSTORM_OPEN_CMD set to a capture command with a ; touch <pwned> tail, asserting <pwned> is never created. Complements the existing assertion that the URL's &/? stay a single argv element.

Also retargeted the PR base maindev per the contribution rules (good flag — I'd missed that). Branch is rebased onto dev, mergeable/CLEAN. Full brainstorm-server suite green (136 tests: 125 node + 11 bash, including both the URL-integrity and the new injection case).

@obra — ready for review when you have a moment. Closes #1957.

muunkky added a commit to muunkky/superpowers that referenced this pull request Jul 14, 2026
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).
@aznikline
aznikline force-pushed the fix/brainstorm-open-cmd-no-shell branch from 0e8622f to b04f985 Compare July 16, 2026 14:26
aznikline added a commit to aznikline/superpowers that referenced this pull request Jul 21, 2026
…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).
aznikline added a commit to aznikline/superpowers that referenced this pull request Jul 21, 2026
…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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modify, or improve documentation for, BRAINSTORM_OPEN, picked by AI security audit

2 participants