Commit d7bc9f6
authored
fix(commands): correct multi-line quoted command parsing, auto-approval, and malformed-command error surfacing (#483)
* fix(commands): treat multi-line quoted argument as a single command for auto-approval
parseCommand split on every newline before any quote handling, so newlines
inside a quoted argument (e.g. a multi-line script passed to sh -c) were treated
as separate commands. Single-quoted and ANSI-C ($'...') strings were also not
fully masked, leaking placeholders and bogus sub-commands. This defeated
allowlist auto-approval and produced a noisy command-pattern breakdown in the UI.
Mask quoted strings (single, double, ANSI-C) before splitting on unquoted
newlines, using a single left-to-right alternation so a quote of one style
inside the other does not start a spurious match. Genuine unquoted newlines
still split into separate sub-commands.
* fix(commands): mask ANSI-C quoted strings in top-level newline split
Handle $'...' (ANSI-C) quoting in parseCommand's pre-split masking so an
escaped apostrophe inside the quoted body no longer terminates the match
early and leak an embedded newline, which would split a single command into
bogus sub-commands. Add a regression test covering an escaped apostrophe
plus newline inside an ANSI-C argument.
* test(commands): cover shell-quote parse-failure fallback restoration
Mock shell-quote parse() to throw and assert the fallback path restores the
ANSI-C single-quote placeholder, closing the patch-coverage gap on the parse-
failure branch.
* fix(commands): reject unterminated-quote commands from auto-approval
Add findUnterminatedQuote, a quote-aware state-machine scanner that detects a command containing an unclosed quote (a shell syntax error, common in LLM-generated commands with nested quotes). parseCommand now returns such input as a single opaque token instead of splitting on embedded newlines, so a line intended to live inside the unclosed quote cannot surface as an independently auto-approvable sub-command.
The scanner returns { quoteType, openIndex } to support a future execution-layer rejection that surfaces a located error to the model; that pre-execution rejection is intentionally deferred to a follow-up.
* fix(commands): make top-level quote masking comment-aware
Replace the cross-line quote-masking regex in parseCommand() with a state machine (maskTopLevelQuotes) that mirrors findUnterminatedQuote. A quote inside a # comment no longer pairs with a quote on a later line, so a comment can no longer hide a real newline separator and merge two distinct commands.
* fix(commands): add heredoc and locale-quote support to command parser; fix pattern extractor
- parseCommand: mask heredocs (<<, <<-, all delimiter quoting styles) as single
atomic tokens before newline splitting; unterminated heredocs returned as opaque token
- parseCommand: add locale-quote ($"...") support alongside existing ANSI-C ($'...')
- findUnterminatedQuote: extend QuoteType with "locale" and "heredoc" variants
- extractPatternsFromCommand (webview): pre-split via parseCommand before shell-quote
tokenization, preventing spurious EOF/body-line/operator tokens in allow/deny selector
- Update changeset to cover all three fix areas
* fix(commands): handle herestring (<<<) in parse-command and findUnterminatedQuote
- Add explicit <<< passthrough in maskTopLevelQuotes: emit all three < chars
verbatim and advance i by 3 so the second < does not re-trigger the heredoc
branch on the next iteration
- Same fix in findUnterminatedQuote for the same root cause
- Add herestring test suite covering single-line, multi-command split, and
single-quoted/ANSI-C quoted multiline word cases
* fix(commands): address CodeRabbit review comments
- findUnterminatedQuote: track stripTabs for <<- so tab-stripping only
applies when the heredoc opener used <<- (consistent with maskTopLevelQuotes)
- findUnterminatedQuote: add doubleIsLocale flag so an unterminated $"..."
region returns quoteType "locale" instead of "double"
- findUnterminatedQuote: add tests for unterminated locale quote and balanced
<<- with indented terminator
- CommandExecution: exclude multi-line opaque tokens (heredoc bodies,
unterminated quotes) from the raw-command pattern set so body-line words
never surface as independently approvable patterns
- CommandExecution.spec: strengthen fragment assertions to use per-fragment
substring checks, exposing the CommandExecution leak bug
* refactor(commands): consolidate quote scanners; add ParseResult; remove dead arrayIndexing
- Unify findUnterminatedQuote and maskTopLevelQuotes under a single
scanTopLevelQuotes state machine -- one pass, no duplicate quoting
logic between the two functions
- Change parseCommand return type from string[] to ParseResult
{ commands: string[]; parseError: UnterminatedQuote | null } so
callers can distinguish a parse error from a normal single-command
result without a separate findUnterminatedQuote call
- getCommandDecision reads parseError from parseCommand instead of
calling findUnterminatedQuote independently; returns the new
malformed_command CommandDecision variant for shell syntax errors
- Remove dead arrayIndexing bucket and __ARRAY_N__ restore (never
populated; caused undefined return when input contained the literal
string __ARRAY_0__)
- Add safety-boundary test: unterminated-quote commands return
malformed_command even when prefix is allowlisted, wildcard, or
the exact command string is on the allowlist
- Add findUnterminatedQuote test: closed quote followed by # comment
with apostrophe returns null (not an open region)
- Update all parseCommand call sites to destructure .commands
* feat(commands): add message field to UnterminatedQuote; surface malformed command as toolError in ExecuteCommandTool
* feat(commands): add error status to CommandExecutionStatus; render error card for malformed commands
* i18n: add malformedCommand translation to all 17 non-English locales
* fix: guard parseCommand against literal placeholder token collisions
Commands containing text like __QUOTE_0__ or __SQUOTE_0__ would be
silently corrupted by restorePlaceholders() -- the restore regexes
would match the literal tokens and substitute array entries (or
'undefined') in their place.
Fix: pre-escape __ -> \x00 in parseCommand before any masking begins,
then post-unescape \x00 -> __ across all output commands at the return.
\x00 (null byte, U+0000) is safe as a sentinel because the OS
terminates command strings at the first \x00, so it can never appear
in real shell command text.
Adds one regression test covering all eight internal placeholder
namespaces.1 parent 8d9c078 commit d7bc9f6
29 files changed
Lines changed: 1318 additions & 51 deletions
File tree
- .changeset
- packages/types/src
- src
- core
- auto-approval
- __tests__
- tools
- webview-ui/src
- components/chat
- __tests__
- i18n/locales
- ca
- de
- en
- es
- fr
- hi
- id
- it
- ja
- ko
- nl
- pl
- pt-BR
- ru
- tr
- vi
- zh-CN
- zh-TW
- utils
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
32 | 37 | | |
33 | 38 | | |
34 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
204 | 205 | | |
205 | 206 | | |
206 | 207 | | |
207 | | - | |
| 208 | + | |
208 | 209 | | |
209 | 210 | | |
210 | 211 | | |
| |||
224 | 225 | | |
225 | 226 | | |
226 | 227 | | |
| 228 | + | |
227 | 229 | | |
228 | 230 | | |
229 | 231 | | |
| |||
262 | 264 | | |
263 | 265 | | |
264 | 266 | | |
265 | | - | |
266 | | - | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
267 | 280 | | |
268 | 281 | | |
269 | 282 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
89 | 105 | | |
90 | 106 | | |
91 | 107 | | |
| |||
0 commit comments