From cc9bb5c06fd1d4d2caac198dfc6eb4acf50cbe4c Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Tue, 20 Oct 2026 22:17:16 +0200 Subject: [PATCH 1/3] feat(prompt): state the command syntax for cmd.exe and PowerShell The system prompt only named the shell path, so rules and habits written for bash silently ran with cmd semantics on Windows. Add a Shell Syntax line for cmd.exe and PowerShell (when execute_command is available) covering command chaining, variables and exit codes. Refs #2083 Co-Authored-By: Claude Sonnet 5.5 --- .../sections/__tests__/system-info.spec.ts | 24 +++++++++++++++ src/core/prompts/sections/system-info.ts | 10 ++++++- src/utils/__tests__/shellDialect.spec.ts | 23 ++++++++++++++ src/utils/shellDialect.ts | 30 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/utils/__tests__/shellDialect.spec.ts create mode 100644 src/utils/shellDialect.ts diff --git a/src/core/prompts/sections/__tests__/system-info.spec.ts b/src/core/prompts/sections/__tests__/system-info.spec.ts index 7c3b53c426..421c3a23b6 100644 --- a/src/core/prompts/sections/__tests__/system-info.spec.ts +++ b/src/core/prompts/sections/__tests__/system-info.spec.ts @@ -10,6 +10,7 @@ vi.mock("../../../../utils/shell", () => ({ })) import { getSystemInfoSection } from "../system-info" +import { getShell } from "../../../../utils/shell" import osName from "os-name" const mockOsName = osName as unknown as ReturnType @@ -101,4 +102,27 @@ describe("getSystemInfoSection", () => { // sentinel string) breaks this exact join. expect(result).toContain("default directory for all tool operations. When the user initially gives you a task") }) + + describe("shell syntax", () => { + const cmdPath = "C:\\Windows\\System32\\cmd.exe" + + it("states the dialect for cmd.exe when execute_command is available", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain(`Default Shell: ${cmdPath} +Shell Syntax: Commands run through Windows cmd.exe`) + }) + + it("omits the dialect without execute_command", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + expect(getSystemInfoSection(mockCwd, policyFor(false))).not.toContain("Shell Syntax") + }) + + it("omits the dialect for POSIX shells", () => { + expect(getSystemInfoSection(mockCwd, policyFor())).not.toContain("Shell Syntax") + }) + }) }) diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 003c555d75..6e7067bf7c 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -2,6 +2,7 @@ import os from "os" import osName from "os-name" import { getShell } from "../../../utils/shell" +import { describeShellDialect } from "../../../utils/shellDialect" import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" @@ -33,12 +34,19 @@ export function getSystemInfoSection(cwd: string, policy: EffectiveToolPolicy): ? " New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory." : "" + const shell = getShell() + const shellDialect = executeCommandAvailable ? describeShellDialect(shell) : undefined + const shellDialectLine = shellDialect + ? ` +Shell Syntax: ${shellDialect}` + : "" + const details = `==== SYSTEM INFORMATION Operating System: ${osInfo} -Default Shell: ${getShell()} +Default Shell: ${shell}${shellDialectLine} Home Directory: ${os.homedir().toPosix()} Current Workspace Directory: ${cwd.toPosix()} diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts new file mode 100644 index 0000000000..c5db9ee722 --- /dev/null +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -0,0 +1,23 @@ +import { describeShellDialect } from "../shellDialect" + +describe("describeShellDialect", () => { + it.each([ + { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe" }, + { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe" }, + { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell" }, + { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell" }, + ])("names the dialect for $shell", ({ shell, expected }) => { + expect(describeShellDialect(shell)).toContain(expected) + }) + + it("warns that ';' does not chain commands in cmd.exe", () => { + expect(describeShellDialect("C:\\Windows\\System32\\cmd.exe")).toContain("`;` is not a separator") + }) + + it.each(["/bin/bash", "/bin/zsh", "C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Windows\\System32\\wsl.exe"])( + "adds nothing for POSIX shell %s", + (shell) => { + expect(describeShellDialect(shell)).toBeUndefined() + }, + ) +}) diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts new file mode 100644 index 0000000000..048f62cd35 --- /dev/null +++ b/src/utils/shellDialect.ts @@ -0,0 +1,30 @@ +/** + * Describes the command syntax of Windows-native shells for the system prompt. + * + * Rules and habits written for bash silently do the wrong thing under these shells + * (e.g. `;` is not a command separator in cmd.exe), so the model is told which + * dialect it is writing for. Returns undefined for POSIX shells, where the shell + * path already names the dialect. + */ +export function describeShellDialect(shellPath: string): string | undefined { + // path.basename follows the host platform; split on both separators so Windows paths work anywhere. + const name = (shellPath.split(/[\\/]/).pop() ?? "").toLowerCase() + + if (name === "cmd.exe" || name === "cmd") { + return ( + "Commands run through Windows cmd.exe, not bash. Use cmd syntax: `&` or `&&` to chain commands " + + "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, `%ERRORLEVEL%` for exit codes, " + + "and `dir`, `type`, `findstr`. POSIX tools may be missing or shadowed by other binaries on PATH." + ) + } + + if (name === "powershell.exe" || name === "pwsh.exe" || name === "powershell" || name === "pwsh") { + return ( + "Commands run through PowerShell, not bash. Use PowerShell syntax: `;` to chain commands, " + + "`$env:VAR` for environment variables, `$LASTEXITCODE` for exit codes, and cmdlets such as " + + "`Get-ChildItem` and `Select-String`." + ) + } + + return undefined +} From 61cf6153516fa9123a0d8bfcf49591388a36acb8 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sun, 25 Oct 2026 10:53:30 +0100 Subject: [PATCH 2/3] fix(prompt): correct PowerShell status guidance and assert dialect syntax Co-Authored-By: Claude Sonnet 5.5 --- src/utils/__tests__/shellDialect.spec.ts | 28 +++++++++++++++++++----- src/utils/shellDialect.ts | 5 +++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts index c5db9ee722..ef6ae5e9dd 100644 --- a/src/utils/__tests__/shellDialect.spec.ts +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -1,13 +1,29 @@ import { describeShellDialect } from "../shellDialect" describe("describeShellDialect", () => { + const cmdSyntax = ["`&`", "`&&`", "`%VAR%`", "`set X=Y`", "`%ERRORLEVEL%`"] + const powershellSyntax = ["`;`", "`$env:VAR`", "`$?`", "`$LASTEXITCODE`"] + it.each([ - { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe" }, - { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe" }, - { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell" }, - { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell" }, - ])("names the dialect for $shell", ({ shell, expected }) => { - expect(describeShellDialect(shell)).toContain(expected) + { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe", syntax: cmdSyntax }, + { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe", syntax: cmdSyntax }, + { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell", syntax: powershellSyntax }, + { + shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + expected: "PowerShell", + syntax: powershellSyntax, + }, + ])("names the dialect and its syntax for $shell", ({ shell, expected, syntax }) => { + const description = describeShellDialect(shell) + + expect(description).toContain(expected) + for (const token of syntax) { + expect(description).toContain(token) + } + }) + + it("tells PowerShell users that a failed cmdlet does not change $LASTEXITCODE", () => { + expect(describeShellDialect("pwsh.exe")).toContain("does not change when a cmdlet fails") }) it("warns that ';' does not chain commands in cmd.exe", () => { diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts index 048f62cd35..f0ce859901 100644 --- a/src/utils/shellDialect.ts +++ b/src/utils/shellDialect.ts @@ -21,8 +21,9 @@ export function describeShellDialect(shellPath: string): string | undefined { if (name === "powershell.exe" || name === "pwsh.exe" || name === "powershell" || name === "pwsh") { return ( "Commands run through PowerShell, not bash. Use PowerShell syntax: `;` to chain commands, " + - "`$env:VAR` for environment variables, `$LASTEXITCODE` for exit codes, and cmdlets such as " + - "`Get-ChildItem` and `Select-String`." + "`$env:VAR` for environment variables, `$?` for whether the last command succeeded " + + "(`$LASTEXITCODE` only holds the exit code of native programs and does not change when a cmdlet fails), " + + "and cmdlets such as `Get-ChildItem` and `Select-String`." ) } From 4f0b824a60b64b44ff6cb7bba1394f048ad7ab1d Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sun, 25 Oct 2026 11:15:44 +0100 Subject: [PATCH 3/3] fix(prompt): qualify cmd.exe exit-code guidance and cover bare shell names Co-Authored-By: Claude Sonnet 5.5 --- src/utils/__tests__/shellDialect.spec.ts | 5 ++++- src/utils/shellDialect.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts index ef6ae5e9dd..17b49f1d53 100644 --- a/src/utils/__tests__/shellDialect.spec.ts +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -1,18 +1,21 @@ import { describeShellDialect } from "../shellDialect" describe("describeShellDialect", () => { - const cmdSyntax = ["`&`", "`&&`", "`%VAR%`", "`set X=Y`", "`%ERRORLEVEL%`"] + const cmdSyntax = ["`&`", "`&&`", "`%VAR%`", "`set X=Y`", "`%ERRORLEVEL%`", "`!ERRORLEVEL!`"] const powershellSyntax = ["`;`", "`$env:VAR`", "`$?`", "`$LASTEXITCODE`"] it.each([ { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe", syntax: cmdSyntax }, { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe", syntax: cmdSyntax }, + { shell: "cmd", expected: "cmd.exe", syntax: cmdSyntax }, { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell", syntax: powershellSyntax }, { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell", syntax: powershellSyntax, }, + { shell: "powershell", expected: "PowerShell", syntax: powershellSyntax }, + { shell: "pwsh", expected: "PowerShell", syntax: powershellSyntax }, ])("names the dialect and its syntax for $shell", ({ shell, expected, syntax }) => { const description = describeShellDialect(shell) diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts index f0ce859901..2966a2e292 100644 --- a/src/utils/shellDialect.ts +++ b/src/utils/shellDialect.ts @@ -13,7 +13,9 @@ export function describeShellDialect(shellPath: string): string | undefined { if (name === "cmd.exe" || name === "cmd") { return ( "Commands run through Windows cmd.exe, not bash. Use cmd syntax: `&` or `&&` to chain commands " + - "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, `%ERRORLEVEL%` for exit codes, " + + "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, and `%ERRORLEVEL%` for exit codes " + + "(`%ERRORLEVEL%` expands before a chained line runs, so use `&&` or `||` for same-line success checks; " + + "enable delayed expansion and use `!ERRORLEVEL!` when the numeric result is needed after a command), " + "and `dir`, `type`, `findstr`. POSIX tools may be missing or shadowed by other binaries on PATH." ) }