What
run_command currently rejects any input containing |, &&, ||, ;, >, 2>&1, etc. — see src/tools/shell.ts:225-230. Models routinely try seq 10 | grep 5 or cmd1 && cmd2 and have to re-plan after the rejection. Discussion #231 raised this.
I want to support the four most common chain operators by parsing them ourselves and spawning each segment as a separate process — without invoking any system shell.
Why not just invoke a shell
Cross-shell semantics make shell:true a non-starter on Windows:
| Operator |
bash/zsh |
PowerShell 5.1 |
PowerShell 7+ |
cmd.exe |
| |
bytes |
objects, not bytes |
objects |
bytes |
&& || |
yes |
parse error |
yes (v7.0+) |
yes |
; |
yes |
yes |
yes |
no |
2>&1 |
merge |
wraps stderr as ErrorRecord on native exes |
same wrapping |
merge |
PS 5.1 is the default shell on Windows 10/11. Letting it parse cmd1 && cmd2 would fail outright. Splitting and spawning ourselves sidesteps all of this.
Proposed approach
Phase 1 — support |, &&, ||, ;:
- Use
shell-quote's parse() to split the input into segments + ops. (Active maintenance, POSIX, no fork, ~10 KB.)
- For each segment, run the existing
isAllowed() check independently. Side effect: granular permissions — git status | grep main passes if both segments are individually allowed, which is exactly what Why not allow command piping and stderr redirection? #231 asked for.
- Spawn each segment via the existing
prepareSpawn() path (still shell:false, keeps .cmd/.bat + PATHEXT + PowerShell UTF-8 handling). Wire stdout→stdin for \|; for &&/\|\|/; chain by exit code.
- Reuse existing timeout / abort / byte-buffer /
smartDecodeOutput logic at the chain level.
Phase 2 (later) — per-segment redirects: > file, >> file, 2>&1, implemented with fs.createWriteStream and stream piping. No shell.
Out of scope — reject with clear error:
<() process substitution
$(…) command substitution
- heredocs
* glob expansion (let grep/find/rg do their own)
- background
&
- PowerShell-specific syntax (
\$env:FOO, @'…'@)
Known compromises
findstr/where/type on Windows are cmd built-ins, not real binaries — they won't resolve. Doc fix: recommend rg/grep. Affects models that emit cmd.exe-style commands.
- Glob expansion not done. Most tools the model uses already glob themselves.
Tests
- Extend
tests/shell-tools.test.ts: chain parsing, per-segment allowlist, exit-code chaining for &&/||, stream piping for |, mixed chains, ordering of ;.
- Reject path tests for everything in "out of scope".
- Cross-platform: keep Windows runner if there is one; otherwise mark Windows-specific behavior with a
process.platform === "win32" guard in tests.
Non-goals
- no shell invocation
- no full bash AST
- no behavior change to single-command path
Tracks discussion #231.
What
run_commandcurrently rejects any input containing|,&&,||,;,>,2>&1, etc. — seesrc/tools/shell.ts:225-230. Models routinely tryseq 10 | grep 5orcmd1 && cmd2and have to re-plan after the rejection. Discussion #231 raised this.I want to support the four most common chain operators by parsing them ourselves and spawning each segment as a separate process — without invoking any system shell.
Why not just invoke a shell
Cross-shell semantics make
shell:truea non-starter on Windows:|&&||;2>&1PS 5.1 is the default shell on Windows 10/11. Letting it parse
cmd1 && cmd2would fail outright. Splitting and spawning ourselves sidesteps all of this.Proposed approach
Phase 1 — support
|,&&,||,;:shell-quote'sparse()to split the input into segments + ops. (Active maintenance, POSIX, no fork, ~10 KB.)isAllowed()check independently. Side effect: granular permissions —git status | grep mainpasses if both segments are individually allowed, which is exactly what Why not allow command piping and stderr redirection? #231 asked for.prepareSpawn()path (stillshell:false, keeps.cmd/.bat+ PATHEXT + PowerShell UTF-8 handling). Wire stdout→stdin for\|; for&&/\|\|/;chain by exit code.smartDecodeOutputlogic at the chain level.Phase 2 (later) — per-segment redirects:
> file,>> file,2>&1, implemented withfs.createWriteStreamand stream piping. No shell.Out of scope — reject with clear error:
<()process substitution$(…)command substitution*glob expansion (letgrep/find/rgdo their own)&\$env:FOO,@'…'@)Known compromises
findstr/where/typeon Windows are cmd built-ins, not real binaries — they won't resolve. Doc fix: recommendrg/grep. Affects models that emit cmd.exe-style commands.Tests
tests/shell-tools.test.ts: chain parsing, per-segment allowlist, exit-code chaining for&&/||, stream piping for|, mixed chains, ordering of;.process.platform === "win32"guard in tests.Non-goals
Tracks discussion #231.