fix: avoid shell-escaping special characters except quotes#7242
fix: avoid shell-escaping special characters except quotes#7242lifeizhou-ap merged 3 commits intomainfrom
Conversation
…scape the whole command line for extension
There was a problem hiding this comment.
Pull request overview
This PR fixes a Windows-specific bug where extensions failed to load due to shell-quote's escaping behavior. The shell-quote library was escaping special characters like @ to \@, which broke Windows command execution for npm packages like @upstash/context7-mcp.
Changes:
- Removed the
shell-quotenpm dependency and its usage - Implemented custom
splitCmdAndArgsandcombineCmdAndArgsfunctions for parsing and formatting command strings - Added tests covering the specific
@character case and roundtrip behavior
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/settings/extensions/utils.ts | Removed shell-quote dependency, implemented custom parsing/quoting logic that doesn't escape special characters unnecessarily |
| ui/desktop/src/components/settings/extensions/utils.test.ts | Added tests for @ character handling and roundtrip conversion |
| ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx | Updated to use new combineCmdAndArgs function for display |
| ui/desktop/package.json | Removed shell-quote dependency |
| ui/desktop/package-lock.json | Removed shell-quote package lock entries |
Files not reviewed (1)
- ui/desktop/package-lock.json: Language not supported
| } else { | ||
| current += ch; | ||
| } | ||
| } |
There was a problem hiding this comment.
The parser silently accepts unclosed quotes. If a user enters npx -y "@upstash/context7-mcp (with unclosed double quote), everything after the opening quote becomes part of a single argument, which may not be the intended behavior. Consider detecting unclosed quotes and either throwing an error or treating the quote as a literal character.
| } | |
| } | |
| // If we reach the end of the string while still inside a quote, treat quotes as literals | |
| // by falling back to a simple whitespace-based split. | |
| if (inSingle || inDouble) { | |
| const fallbackParts = trimmed.split(/\s+/); | |
| return { cmd: fallbackParts[0] || '', args: fallbackParts.slice(1) }; | |
| } |
| return [cmd, ...args] | ||
| .map((a) => { | ||
| if (!a.includes(' ')) return a; | ||
| if (a.includes('"')) return `'${a}'`; |
There was a problem hiding this comment.
The quoting logic has a flaw: if an argument contains both a space and a double quote, it wraps the arg in single quotes. However, if that arg also contains a single quote (e.g., it's "quoted"), the round-trip through combineCmdAndArgs and splitCmdAndArgs will fail because splitCmdAndArgs will end the quote at the apostrophe. Consider implementing escape sequences for quotes, or using a more robust approach that can handle all combinations of special characters.
| if (a.includes('"')) return `'${a}'`; | |
| if (a.includes('"')) { | |
| // If the argument contains both single and double quotes, escape double quotes and use double quotes outside | |
| if (a.includes("'")) { | |
| const escaped = a.replace(/"/g, '\\"'); | |
| return `"${escaped}"`; | |
| } | |
| // Safe to wrap in single quotes when there are no embedded single quotes | |
| return `'${a}'`; | |
| } |
| export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } { | ||
| const trimmed = str.trim(); | ||
| if (!trimmed) { | ||
| return { cmd: '', args: [] }; | ||
| if (!trimmed) return { cmd: '', args: [] }; | ||
|
|
||
| const parts: string[] = []; | ||
| let current = ''; | ||
| let inSingle = false; | ||
| let inDouble = false; | ||
|
|
||
| for (const ch of trimmed) { | ||
| if (inSingle) { | ||
| if (ch === "'") inSingle = false; | ||
| else current += ch; | ||
| } else if (inDouble) { | ||
| if (ch === '"') inDouble = false; | ||
| else current += ch; | ||
| } else if (ch === "'") { | ||
| inSingle = true; | ||
| } else if (ch === '"') { | ||
| inDouble = true; | ||
| } else if (/\s/.test(ch)) { | ||
| if (current) { | ||
| parts.push(current); | ||
| current = ''; | ||
| } | ||
| } else { | ||
| current += ch; | ||
| } | ||
| } | ||
| if (current) parts.push(current); | ||
|
|
||
| const parsed = parseShellQuote(trimmed); | ||
| const words = parsed.filter((item): item is string => typeof item === 'string').map(String); | ||
|
|
||
| const cmd = words[0] || ''; | ||
| const args = words.slice(1); | ||
|
|
||
| return { | ||
| cmd, | ||
| args, | ||
| }; | ||
| return { cmd: parts[0] || '', args: parts.slice(1) }; | ||
| } |
There was a problem hiding this comment.
The new quote parsing logic lacks test coverage for edge cases like unclosed quotes (e.g., npx -y "@upstash/context7-mcp), arguments with both single and double quotes (e.g., it's "quoted"), and empty quotes. Given that shell-quote was replaced with custom parsing, these edge cases should be explicitly tested to prevent regressions.
…our to escape the whole command line for extension" This reverts commit 2a742f5.
* main: fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242)
* origin/main: docs: playwright CLI skill tutorial (#7261) install node in goose dir (#7220) fix: relax test_basic_response assertion for providers returning reasoning_content (#7249) fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242) fix: use dynamic port for Tetrate auth callback server (#7228) docs: removing LLM Usage admonitions (#7227) feat(otel): respect standard OTel env vars for exporter selection (#7144) fix: fork session (#7219) Bump version numbers for 1.24.0 release (#7214) Move platform extensions into their own folder (#7210) fix: ignore deprecated skills extension (#7139) # Conflicts: # Cargo.lock # Cargo.toml
…led-extensions-cmd * 'main' of github.com:block/goose: (24 commits) Set up direnv and update flake inputs (#6526) fix: restore subagent tool call notifications after summon refactor (#7243) fix(ui): preserve server config values on partial provider config save (#7248) fix(claude-code): allow goose to run inside a Claude Code session (#7232) fix(openai): route gpt-5 codex via responses and map base paths (#7254) feat: add GoosePlatform to AgentConfig and MCP initialization (#6931) Fix copied over (#7270) feat(gemini-cli): add streaming support via stream-json events (#7244) fix: filter models without tool support from recommended list (#7198) fix(google): handle more thoughtSignature vagaries during streaming (#7204) docs: playwright CLI skill tutorial (#7261) install node in goose dir (#7220) fix: relax test_basic_response assertion for providers returning reasoning_content (#7249) fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242) fix: use dynamic port for Tetrate auth callback server (#7228) ...
Summary
Fixed #6816 with the display issue.
Replace shell-quote's quote() with a simple quoting function for displaying extension commands in the UI. The shell quote function does more escape which is not necessary for display eg: "@" -> "//@". Keep shell-quote's parse() for command parsing on user input.
Type of Change
Testing
Unit test and manual testing
Screenshots/Demos (for UX changes)
Before:

After:
