feat: unify CLI execution and never spawn a shell#949
Open
EhabY wants to merge 1 commit into
Open
Conversation
8e8bbc3 to
b195482
Compare
3dc671f to
dfa3c13
Compare
Drops the last Node-level \`shell: true\` from the extension and replaces
the behaviours we relied on the shell for with explicit expansion.
Spawns \`coder\` as an argv array (\`spawn(binary, args)\`) instead of a
quoted command string through a shell. Args reach \`coder\` byte-for-byte:
no Windows \`cmd.exe\` metacharacter escaping surface for server-supplied
template parameter values, no surprise re-parsing on Linux/macOS, one
code path across platforms.
Also reinstates \`proc.on("error", reject)\` so a missing/unexecutable
binary surfaces via the spawn error event (Node emits \`error\` for
ENOENT/EACCES when there is no shell) instead of hanging the progress
dialog.
Same treatment: \`spawn(binary, argv, { detached })\` instead of
\`spawn(cmdString, { shell: true, detached })\`. The process-group setup
for Ctrl+C handling comes from \`detached\` alone — the shell wrapper was
not contributing anything. \`ping\` now uses \`getGlobalFlags\` (raw) and
passes the workspace name unescaped, matching the rest of the codebase.
Two surfaces still feed a shell because the consumer runs one:
- \`openAppStatusTerminal\` (VS Code \`Terminal\` → user's shell).
- \`buildProxyCommand\` (string handed to OpenSSH's \`ProxyCommand\`).
Both still use \`getGlobalShellFlags\` + the platform-appropriate
escaper. That's correct — they're outside Node's \`shell: true\` and
escaping is required by the consuming shell.
With no shell to expand values, the extension now substitutes:
- \`\${env:VAR}\` from \`process.env\` (missing → empty, matching VS
Code's own \`\${env:VAR}\` semantics).
- Leading \`~\` and \`\${userHome}\` from \`os.homedir()\`. For
\`--flag=value\` entries the expansion is scoped to the value half
so \`--cfg=~/coder\` works.
Substitution lives in \`getUserGlobalFlags\`, which feeds both
\`getGlobalFlags\` and \`getGlobalShellFlags\`, so every site that reads
the user's flags gets the same expansion. The doc in package.json
spells out exactly which forms are supported.
- \`cliConfig.test.ts\`: existing \`\${env:VAR}\` test plus a new test
covering tilde / \`\${userHome}\` expansion (bare \`~\` entry,
\`--flag=~/value\`, \`\${userHome}\` mid-string, mid-value tildes
left alone).
- The test-side \`shellQuote\` mirror added during the temporary
shell-revert is dropped along with its tests — no caller needs it
now that no test asserts an \`escapeShellArg\` output.
\`getGlobalShellFlags\`'s comment loses the \`spawn({ shell: true })\`
mention since that no longer exists; only \`terminal.sendText\` and SSH
\`ProxyCommand\` remain as shell contexts.
b195482 to
fe65639
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Drops the last Node-level
shell: truefrom the extension and replaces the behaviours we relied on the shell for with explicit expansion. Stacks on top of #945 — merge that one first.runCliCommand(start / update)Spawns
coderas an argv array (spawn(binary, args)) instead of a quoted command string through a shell. Args reachcoderbyte-for-byte: no Windowscmd.exemetacharacter escaping surface for server-supplied template parameter values, no surprise re-parsing on Linux/macOS, one code path across platforms.Reinstates
proc.on("error", reject)so a missing/unexecutable binary surfaces via the spawn error event (Node emitserrorfor ENOENT/EACCES when there is no shell) instead of hanging the progress dialog.coder ping(spawnCliInTerminal)Same treatment:
spawn(binary, argv, { detached })instead ofspawn(cmdString, { shell: true, detached }). The process-group setup for Ctrl+C handling comes fromdetachedalone — the shell wrapper wasn't contributing anything.pingnow usesgetGlobalFlags(raw) and passes the workspace name unescaped, matching the rest of the codebase.Shell-context call sites we don't own
Two surfaces still feed a shell because the consumer runs one, and these are intentionally left alone:
openAppStatusTerminal(VS CodeTerminal→ user's shell).buildProxyCommand(string handed to OpenSSH'sProxyCommand).Both still use
getGlobalShellFlags+ the platform-appropriate escaper. That's correct — they're outside Node'sshell: trueand escaping is required by the consuming shell.Expansion in
coder.globalFlagsWith no shell to expand values, the extension now substitutes:
${env:VAR}fromprocess.env(missing → empty, matching VS Code's own${env:VAR}semantics).~and${userHome}→os.homedir(). For--flag=valueentries the path expansion is scoped to the value half so--cfg=~/coderworks.Substitution lives in
getUserGlobalFlags, which feeds bothgetGlobalFlagsandgetGlobalShellFlags, so every site that reads the user's flags gets the same expansion.package.jsonspells out exactly which forms are supported.Why split this from #945
The shell-mode change is the only piece in the original PR that touches a security-sensitive boundary (server-controlled template parameter values reaching a Windows shell). Splitting it out means it can be reverted on its own if needed without losing the parameter-prompting work.
Pre-existing hardening (
proc.stdin.end(), signal-aware close handler,getHeaderArgsescape threading) stays on #945 because it's shell-mode-agnostic — same behaviour either way.Test plan
pnpm test— 1578 passing.pnpm lint,pnpm typecheck— clean.coder start/coder updatesucceed and stream logs.coder ping <workspace>from the command palette, confirm Ctrl+C kills it cleanly.coder.globalFlagsto["--cfg=~/coder", "--token=${env:CODER_SESSION_TOKEN}"]and verify the values reach the CLI expanded.coder updateagainst a template whose required parameters include shell metacharacters in their names or values; confirm they flow through unescaped.