Skip to content

fix: quote dropped and pasted paths for the pane's shell, not the host platform - #432

Open
dormouse-bot wants to merge 4 commits into
mainfrom
fix/issue-430
Open

fix: quote dropped and pasted paths for the pane's shell, not the host platform#432
dormouse-bot wants to merge 4 commits into
mainfrom
fix/issue-430

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

shellEscapePath chose its quoting rule from IS_WINDOWS, so every pane on a Windows host got cmd.exe quoting — double quotes with embedded " doubled. That is sound for cmd.exe, which does no expansion inside double quotes, but a PowerShell double-quoted string is expandable: $(...) runs a subexpression and $name interpolates. Dropping or pasting a file named $(calc.exe).txt into a PowerShell pane staged the line "$(calc.exe).txt", and the subexpression ran the moment the user pressed Enter — which they have every reason to do, since they just dropped the file in to use its path. Git Bash and WSL panes on Windows had the same parser mismatch (minus the execution): they read posix quoting, not cmd quoting.

Solution

Quote for the shell that will parse the line, not for the host platform. shellEscapePath now takes a ShellCommandKind and dispatches three ways — posix backslash-escaping, cmd double quotes, PowerShell single-quoted literals via dor's quotePowerShellArg (exported for this).

The kind comes from the Session, not from the app-global default, because a Windows user can keep PowerShell, Git Bash, WSL, and cmd.exe panes live at the same time and change the default for future terminals at any point. Each TerminalEntry captures a shellKind when it is created, classified from the shell that Session actually launched with:

  • Fresh spawn — the pending shell opts already carried the shell; the same value now feeds shellCommandKind and spawnPty.
  • Cold restoresession-restore.ts already passed the current default into restoreTerminal; that shell is now captured too.
  • Live reconnectpty:list gains an optional shell field carrying each live PTY's launch shell, so a webview reload rebuilds the same kind. The standalone sidecar reports it post-resolveSpawnConfig (a default-shell PTY reports the resolved ComSpec / $SHELL, not undefined); the VS Code extension host buffers the requested shell, whose absence resolves through the same pty-core module and so classifies identically.

Only a paste into an id with no registry entry falls back to the app-global shell, then to the platform — so cmd panes and every posix host keep exactly their current escaping.

quotePowerShellArg's bare-argument set also had to lose ,: PowerShell's argument mode reads a bare comma as the array operator, so C:\a,b.png would have reached the command as two arguments — a mis-tokenization the old cmd-style double quoting handled by accident. @ went with it (splatting / array / hashtable syntax at the head of a token). cmd.exe treats , as an argument separator and @ as echo suppression, so the shared WINDOWS_SAFE_ARG fix covers quoteCmdArg (and therefore dor's own cmd command quoting) as well.

greping for the same platform-infers-shell pattern elsewhere turned up nothing — the other IS_WINDOWS reads (win32-input-mode, the updater's sidecar kill) are genuinely about the host, not about a shell parser.

Testing

lib/src/lib/shell-escape.test.ts covers the dispatch directly: PowerShell subexpressions, $env: interpolation, embedded-' doubling, comma- and @-bearing paths, bare inert paths, and a posix (Git Bash / WSL) Session on Windows. Around it, clipboard.test.ts pins that a PowerShell Session still gets PowerShell quoting after the app default changes, terminal-registry.alert.test.ts pins capture on both spawn and resume, reconnect.test.ts pins the shell field surviving pty:list, pty-core.test.js pins the sidecar reporting it, and cli-output.test.mjs pins the classifier's platform fallback. Full lib, dor, and sidecar suites green; tsc -b clean.

Specs

docs/specs/mouse-and-clipboard.md §8.6 gains the per-shell escaping rules and the Session-kind selection order it was missing. docs/specs/transport.md documents the new pty:list field, and docs/specs/dor-cli.md cross-links to §8.6 since both now go through the same classifier.


Closes #430 — automated triage

…atform

shellEscapePath picked cmd.exe quoting from IS_WINDOWS alone, so a PowerShell
pane got a double-quoted path — and PowerShell double quotes are expandable.
Dropping a file named `$(calc.exe).txt` staged `"$(calc.exe).txt"`, which ran
the subexpression as soon as the user pressed Enter, which is the whole point
of dropping a file in. Git Bash and WSL panes on Windows had the same
mismatch, minus the execution.

Dispatch on the selected shell instead, classified by dor's shellCommandKind
(the app-global default stands in for the pane's shell, as dor's own command
quoting already does), and reuse dor's quotePowerShellArg for the PowerShell
branch. With no shell selected the host platform is still the fallback, so
cmd panes and every posix host keep their current escaping.

Closes #430
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6a7e5ff
Status: ✅  Deploy successful!
Preview URL: https://aac870f4.mouseterm.pages.dev
Branch Preview URL: https://fix-issue-430.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review — the dispatch itself reads correctly (the cmd/no-shell fallback preserves today's behavior exactly, and the app-global stand-in is the only signal available: PersistedPane carries no shell and terminal-state's shellName is a display label, so there is no per-pane shell to thread). One narrow regression in the reused helper, flagged inline.

Also: paneShellKind's if (!shell) return !IS_MAC && IS_WINDOWS ? 'cmd' : 'posix' re-implements the fallback shellCommandKind already has (if (!normalizedShell && /win/i.test(platformString)) return 'cmd'), so the "Windows means cmd" rule now lives in two places and can drift. use-dor-control.ts's dorCommandString already relies on the built-in one — passing a possibly-undefined shell straight through.

I'm the author here, so rather than leave these as suggestions nobody will apply, I'll push both fixes to this branch.

Comment thread dor/src/commands/shell-quote.ts
dormouse-bot and others added 2 commits August 21, 2026 16:06
…back

Two self-review findings on the previous commit.

`quotePowerShellArg`'s `WINDOWS_SAFE_ARG` allowed `,`, so a path built only
from safe characters was returned bare — and PowerShell's argument mode reads
a bare comma as the array operator, so `C:\a,b.png` reached the command as two
arguments. Routing dropped paths through this helper would therefore have
traded the `$(...)` execution bug for a mis-tokenization on comma-bearing
filenames, which the old cmd-style double quoting handled by accident.
cmd.exe treats `,` as an argument separator too, so `quoteCmdArg` shared the
hole; dropping `,` from the shared regex closes both and only ever makes the
output more conservative.

`paneShellKind` re-implemented the platform fallback that `shellCommandKind`
already has, putting "an unset shell means cmd on Windows" in two places.
Call the classifier with the possibly-undefined shell instead, the way
`use-dor-control.ts`'s `dorCommandString` already does.

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after c7f3514, which replaced the app-global stand-in with per-Session shell capture. The plumbing holds end to end: the sidecar stores config.shell after resolveSpawnConfig, so a default-shell PTY reports the resolved ComSpec / $SHELL rather than undefined; the ptys.get(id) === p generation guard already covers ptyShells cleanup on a respawn under the same id; VS Code's pty-manager buffers the requested shell instead, but its PTY host is the same pty-core module (vscode-ext/src/pty-host.js requires lib/pty-core.cjs), so an absent value resolves to cmd.exe on Windows and classifies the same on both hosts — the claim the new PtyBufferEntry.shell comment makes. Cold restore already passed shellOpts?.shell into restoreTerminal, and Tauri's Rust bridge forwards the sidecar's list payload as raw JSON, so neither needed a change to carry the new field.

One test gap, inline: @ was dropped from WINDOWS_SAFE_ARG alongside ,, and the comment justifies both by cmd.exe behavior as much as by PowerShell's — but the only assertion added covers the PowerShell consumer. quoteCmdArg shares the constant and nothing pins its output for either character.

Separately, the PR description still describes the superseded design ("the app-global selected shell stands in for the pane's shell, which is not tracked per-session") and doesn't mention the pty:list shell field or the registry capture that replaced it. I'm updating it in place.

Comment thread dor/test/cli-output.test.mjs
quoteCmdArg shares WINDOWS_SAFE_ARG with quotePowerShellArg, so dropping
, and @ from the set changed cmd output too; only the PowerShell side was
asserted.
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.

Dropped file paths are quoted for cmd.exe even in a PowerShell pane, allowing subexpression execution

2 participants