From 24d6398bea25ef78ce13fb8fa9a4d915e20d9f26 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Tue, 20 Oct 2026 22:17:16 +0200 Subject: [PATCH 1/2] 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 bab4c960e5bdbffc66783b64c05a031a774ba533 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Tue, 20 Oct 2026 22:24:01 +0200 Subject: [PATCH 2/2] fix(dcg): name the assumed shell in blocks and enable Windows packs DCG parses every command as POSIX, so on cmd.exe/PowerShell a block can mean the command was unreadable rather than destructive. Append a note saying so, and enable DCG's windows.filesystem/windows.misc packs on Windows so cmd-only destructive forms (rd /s, format) are matched. Refs #2082 Co-Authored-By: Claude Sonnet 5.5 --- src/core/tools/ExecuteCommandTool.ts | 39 +++++++++++++------ .../__tests__/executeCommandTool.spec.ts | 14 +++++++ src/i18n/locales/ca/tools.json | 3 +- src/i18n/locales/de/tools.json | 3 +- src/i18n/locales/en/tools.json | 3 +- src/i18n/locales/es/tools.json | 3 +- src/i18n/locales/fr/tools.json | 3 +- src/i18n/locales/hi/tools.json | 3 +- src/i18n/locales/id/tools.json | 3 +- src/i18n/locales/it/tools.json | 3 +- src/i18n/locales/ja/tools.json | 3 +- src/i18n/locales/ko/tools.json | 3 +- src/i18n/locales/nl/tools.json | 3 +- src/i18n/locales/pl/tools.json | 3 +- src/i18n/locales/pt-BR/tools.json | 3 +- src/i18n/locales/ru/tools.json | 3 +- src/i18n/locales/tr/tools.json | 3 +- src/i18n/locales/vi/tools.json | 3 +- src/i18n/locales/zh-CN/tools.json | 3 +- src/i18n/locales/zh-TW/tools.json | 3 +- .../__tests__/runner.spec.ts | 20 +++++++++- .../destructive-command-guard/runner.ts | 10 ++++- src/utils/shellDialect.ts | 8 +++- 23 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index 9855e6e417..28d5574bc0 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -31,6 +31,8 @@ import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor import { Package } from "../../shared/package" import { t } from "../../i18n" import { getTaskDirectoryPath } from "../../utils/storage" +import { getShell } from "../../utils/shell" +import { describeShellDialect, getShellName } from "../../utils/shellDialect" import { BaseTool, ToolCallbacks } from "./BaseTool" export { ShellIntegrationError } from "../../integrations/terminal/types" @@ -55,20 +57,35 @@ interface ExecuteCommandParams { timeout?: number | null } -export function formatDcgBlockedMessage(reason?: string, ruleId?: string): string { - if (reason && ruleId) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId }) - } +/** + * @param shellPath The shell the command will run in. DCG parses every command as POSIX, + * so for cmd.exe/PowerShell the message says so, letting the user tell "dangerous" from "unreadable". + */ +export function formatDcgBlockedMessage(reason?: string, ruleId?: string, shellPath?: string): string { + const message = (() => { + if (reason && ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId }) + } - if (reason) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason }) - } + if (reason) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason }) + } + + if (ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId }) + } - if (ruleId) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId }) + return t("tools:executeCommand.destructiveCommandGuard.blocked") + })() + + if (shellPath && describeShellDialect(shellPath)) { + const note = t("tools:executeCommand.destructiveCommandGuard.assumedPosixNote", { + shell: getShellName(shellPath), + }) + return `${message} ${note}` } - return t("tools:executeCommand.destructiveCommandGuard.blocked") + return message } export function resolveAgentTimeoutMs(timeoutSeconds: number | null | undefined): number { @@ -182,7 +199,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { // retryable tool_error via the outer catch — never as a policy denial. dcgDecision = await runDcg(binaryPath, canonicalCommand, workingDirectory) if (dcgDecision.decision === "deny") { - await task.say("error", formatDcgBlockedMessage(dcgDecision.reason, dcgDecision.ruleId)) + await task.say("error", formatDcgBlockedMessage(dcgDecision.reason, dcgDecision.ruleId, getShell())) } } diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index c580b55102..563afef3ed 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -71,6 +71,9 @@ vitest.mock("../../../services/destructive-command-guard", () => ({ // Import the module import * as executeCommandModule from "../ExecuteCommandTool" + +// The DCG block message names the executing shell; pin it so tests do not depend on the host. +vi.mock("../../../utils/shell", () => ({ getShell: vi.fn(() => "/bin/bash") })) const { executeCommandTool } = executeCommandModule // Spies installed on the auto-approval module are not restored between tests, @@ -316,6 +319,17 @@ describe("executeCommandTool", () => { expect(executeCommandModule.formatDcgBlockedMessage(reason, ruleId)).toBe(expected) }) + it.each([ + ["C:\\Windows\\System32\\cmd.exe", true], + ["C:\\Program Files\\PowerShell\\7\\pwsh.exe", true], + ["/bin/bash", false], + [undefined, false], + ])("appends the POSIX-assumption note only for non-POSIX shell %s", (shellPath, expectNote) => { + const message = executeCommandModule.formatDcgBlockedMessage(undefined, undefined, shellPath) + + expect(message.includes("executeCommand.destructiveCommandGuard.assumedPosixNote")).toBe(expectNote) + }) + it("shows a DCG block message as an error before requesting explicit approval", async () => { const provider = await mockCline.providerRef.deref() provider.context = { globalStorageUri: { fsPath: "/test/storage" } } diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 6e716c4955..f2a1b24a34 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ha bloquejat aquesta ordre.", "blockedWithReason": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard ha bloquejat aquesta ordre. (Regla: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}} (Regla: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}} (Regla: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analitza les ordres com a sintaxi d'intèrpret POSIX, però aquesta ordre s'executa a {{shell}}, de manera que un bloqueig pot voler dir que DCG no ha pogut llegir l'ordre i no que sigui destructiva." } }, "newTask": { diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 695f0b444e..6e40fd6ade 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard hat diesen Befehl blockiert.", "blockedWithReason": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard hat diesen Befehl blockiert. (Regel: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}} (Regel: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}} (Regel: {{ruleId}})", + "assumedPosixNote": "Hinweis: DCG analysiert Befehle als POSIX-Shell-Syntax, dieser Befehl läuft jedoch in {{shell}}. Eine Blockierung kann daher bedeuten, dass DCG den Befehl nicht lesen konnte, und nicht, dass er destruktiv ist." } }, "newTask": { diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 306e71ac86..602b036b72 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard blocked this command.", "blockedWithReason": "Destructive Command Guard blocked this command. Message from DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard blocked this command. (Rule: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})", + "assumedPosixNote": "Note: DCG analyzes commands as POSIX shell syntax, but this command runs in {{shell}}, so a block may mean DCG could not read the command rather than that it is destructive." } }, "newTask": { diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 07cc77d154..e510566530 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard bloqueó este comando.", "blockedWithReason": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard bloqueó este comando. (Regla: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}} (Regla: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}} (Regla: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analiza los comandos como sintaxis de shell POSIX, pero este comando se ejecuta en {{shell}}, por lo que un bloqueo puede significar que DCG no pudo leer el comando y no que sea destructivo." } }, "newTask": { diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index d5d09dce47..a2657d403e 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard a bloqué cette commande.", "blockedWithReason": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}}", "blockedWithRule": "Destructive Command Guard a bloqué cette commande. (Règle : {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}} (Règle : {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}} (Règle : {{ruleId}})", + "assumedPosixNote": "Remarque : DCG analyse les commandes comme de la syntaxe shell POSIX, mais cette commande s'exécute dans {{shell}} ; un blocage peut donc signifier que DCG n'a pas pu lire la commande, et non qu'elle est destructrice." } }, "newTask": { diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 69cf79e6d2..59eb172699 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया।", "blockedWithReason": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}}", "blockedWithRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। (नियम: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}} (नियम: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}} (नियम: {{ruleId}})", + "assumedPosixNote": "नोट: DCG कमांड को POSIX शेल सिंटैक्स के रूप में विश्लेषित करता है, लेकिन यह कमांड {{shell}} में चलती है, इसलिए ब्लॉक का मतलब यह हो सकता है कि DCG कमांड को पढ़ नहीं सका, न कि वह विनाशकारी है।" } }, "newTask": { diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 0f7c9b7fdf..20eeb9ce77 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard memblokir perintah ini.", "blockedWithReason": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard memblokir perintah ini. (Aturan: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}} (Aturan: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}} (Aturan: {{ruleId}})", + "assumedPosixNote": "Catatan: DCG menganalisis perintah sebagai sintaks shell POSIX, tetapi perintah ini berjalan di {{shell}}, sehingga pemblokiran bisa berarti DCG tidak dapat membaca perintah, bukan bahwa perintah tersebut destruktif." } }, "searchFiles": { diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index b0051abf59..8fce341099 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ha bloccato questo comando.", "blockedWithReason": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard ha bloccato questo comando. (Regola: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}} (Regola: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}} (Regola: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analizza i comandi come sintassi shell POSIX, ma questo comando viene eseguito in {{shell}}, quindi un blocco può significare che DCG non è riuscito a leggere il comando, non che sia distruttivo." } }, "newTask": { diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index fa404f7576..c3dc030536 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard がこのコマンドをブロックしました。", "blockedWithReason": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}", "blockedWithRule": "Destructive Command Guard がこのコマンドをブロックしました。(ルール: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}(ルール: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}(ルール: {{ruleId}})", + "assumedPosixNote": "注: DCG はコマンドを POSIX シェル構文として解析しますが、このコマンドは {{shell}} で実行されます。ブロックは、コマンドが破壊的であるためではなく、DCG がコマンドを読み取れなかったことを意味する場合があります。" } }, "newTask": { diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 39335eb6e4..d43ecf0573 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard가 이 명령어를 차단했습니다.", "blockedWithReason": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}}", "blockedWithRule": "Destructive Command Guard가 이 명령어를 차단했습니다. (규칙: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}} (규칙: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}} (규칙: {{ruleId}})", + "assumedPosixNote": "참고: DCG는 명령을 POSIX 셸 구문으로 분석하지만 이 명령은 {{shell}}에서 실행됩니다. 따라서 차단은 명령이 파괴적이라는 뜻이 아니라 DCG가 명령을 읽지 못했다는 뜻일 수 있습니다." } }, "newTask": { diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index d68de44d80..329e052b88 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard heeft deze opdracht geblokkeerd.", "blockedWithReason": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. (Regel: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}} (Regel: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}} (Regel: {{ruleId}})", + "assumedPosixNote": "Opmerking: DCG analyseert opdrachten als POSIX-shellsyntaxis, maar deze opdracht wordt uitgevoerd in {{shell}}. Een blokkade kan dus betekenen dat DCG de opdracht niet kon lezen, niet dat deze destructief is." } }, "newTask": { diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index f7876025fc..e1f2c4777c 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard zablokował to polecenie.", "blockedWithReason": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard zablokował to polecenie. (Reguła: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}} (Reguła: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}} (Reguła: {{ruleId}})", + "assumedPosixNote": "Uwaga: DCG analizuje polecenia jako składnię powłoki POSIX, ale to polecenie jest uruchamiane w {{shell}}, więc blokada może oznaczać, że DCG nie mógł odczytać polecenia, a nie że jest ono destrukcyjne." } }, "newTask": { diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 3b423d3784..8e63cbff7a 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -19,7 +19,8 @@ "blocked": "O Destructive Command Guard bloqueou este comando.", "blockedWithReason": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}}", "blockedWithRule": "O Destructive Command Guard bloqueou este comando. (Regra: {{ruleId}})", - "blockedWithReasonAndRule": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}} (Regra: {{ruleId}})" + "blockedWithReasonAndRule": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}} (Regra: {{ruleId}})", + "assumedPosixNote": "Observação: o DCG analisa comandos como sintaxe de shell POSIX, mas este comando é executado em {{shell}}, então um bloqueio pode significar que o DCG não conseguiu ler o comando, e não que ele seja destrutivo." } }, "newTask": { diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index ff8db8fe2e..c30473432d 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard заблокировал эту команду.", "blockedWithReason": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard заблокировал эту команду. (Правило: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}} (Правило: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}} (Правило: {{ruleId}})", + "assumedPosixNote": "Примечание: DCG анализирует команды как синтаксис оболочки POSIX, но эта команда выполняется в {{shell}}, поэтому блокировка может означать, что DCG не смог прочитать команду, а не то, что она разрушительна." } }, "newTask": { diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 4769a826b4..7017425595 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard bu komutu engelledi.", "blockedWithReason": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}}", "blockedWithRule": "Destructive Command Guard bu komutu engelledi. (Kural: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}} (Kural: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}} (Kural: {{ruleId}})", + "assumedPosixNote": "Not: DCG komutları POSIX kabuk sözdizimi olarak analiz eder, ancak bu komut {{shell}} içinde çalışır; bu nedenle bir engelleme, komutun yıkıcı olduğunu değil DCG'nin komutu okuyamadığını gösteriyor olabilir." } }, "newTask": { diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index d50dd50c65..0b546060a5 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard đã chặn lệnh này.", "blockedWithReason": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard đã chặn lệnh này. (Quy tắc: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}} (Quy tắc: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}} (Quy tắc: {{ruleId}})", + "assumedPosixNote": "Lưu ý: DCG phân tích lệnh theo cú pháp shell POSIX, nhưng lệnh này chạy trong {{shell}}, vì vậy việc chặn có thể nghĩa là DCG không đọc được lệnh chứ không phải lệnh đó mang tính phá hủy." } }, "newTask": { diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 2af580bb6a..8bf46565de 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard 已拦截此命令。", "blockedWithReason": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}", "blockedWithRule": "Destructive Command Guard 已拦截此命令。(规则:{{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}(规则:{{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}(规则:{{ruleId}})", + "assumedPosixNote": "注意:DCG 按 POSIX shell 语法分析命令,但此命令在 {{shell}} 中运行,因此拦截可能意味着 DCG 无法解析该命令,而不是该命令具有破坏性。" } }, "newTask": { diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 75c8047945..b838b39677 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard 已阻擋此命令。", "blockedWithReason": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}", "blockedWithRule": "Destructive Command Guard 已阻擋此命令。(規則:{{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}(規則:{{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}(規則:{{ruleId}})", + "assumedPosixNote": "注意:DCG 以 POSIX shell 語法分析指令,但此指令在 {{shell}} 中執行,因此封鎖可能代表 DCG 無法解析該指令,而不是該指令具有破壞性。" } }, "newTask": { diff --git a/src/services/destructive-command-guard/__tests__/runner.spec.ts b/src/services/destructive-command-guard/__tests__/runner.spec.ts index ddf991f5f6..3a023229dc 100644 --- a/src/services/destructive-command-guard/__tests__/runner.spec.ts +++ b/src/services/destructive-command-guard/__tests__/runner.spec.ts @@ -4,7 +4,7 @@ import { PassThrough } from "stream" import { spawn } from "child_process" import { DCG_MAX_OUTPUT_BYTES } from "../constants" -import { runDcg } from "../runner" +import { getDcgArgs, runDcg } from "../runner" vi.mock("child_process", () => ({ spawn: vi.fn() })) @@ -167,3 +167,21 @@ describe("runDcg", () => { expect(child.kill).toHaveBeenCalledWith("SIGKILL") }) }) + +describe("getDcgArgs", () => { + it("enables the Windows packs on win32", () => { + expect(getDcgArgs("rd /s /q foo", "win32")).toEqual([ + "test", + "--format", + "json", + "--no-color", + "--with-packs", + "windows.filesystem,windows.misc", + "rd /s /q foo", + ]) + }) + + it.each(["linux", "darwin"] as const)("passes no pack options on %s", (platform) => { + expect(getDcgArgs("ls", platform)).toEqual(["test", "--format", "json", "--no-color", "ls"]) + }) +}) diff --git a/src/services/destructive-command-guard/runner.ts b/src/services/destructive-command-guard/runner.ts index 6dd8d4663b..b5830f8887 100644 --- a/src/services/destructive-command-guard/runner.ts +++ b/src/services/destructive-command-guard/runner.ts @@ -23,9 +23,17 @@ function getDcgEnvironment(): NodeJS.ProcessEnv { return env } +// These packs are off by default; they cover cmd.exe forms such as `rd /s /q` and `format`. +const DCG_WINDOWS_PACKS = "windows.filesystem,windows.misc" + +export function getDcgArgs(command: string, platform: NodeJS.Platform = process.platform): string[] { + const packArgs = platform === "win32" ? ["--with-packs", DCG_WINDOWS_PACKS] : [] + return ["test", "--format", "json", "--no-color", ...packArgs, command] +} + export function runDcg(binaryPath: string, command: string, cwd: string): Promise { return new Promise((resolve, reject) => { - const child = spawn(binaryPath, ["test", "--format", "json", "--no-color", command], { + const child = spawn(binaryPath, getDcgArgs(command), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts index 048f62cd35..31f65ce2b7 100644 --- a/src/utils/shellDialect.ts +++ b/src/utils/shellDialect.ts @@ -6,9 +6,13 @@ * 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 { +export function getShellName(shellPath: string): string { // path.basename follows the host platform; split on both separators so Windows paths work anywhere. - const name = (shellPath.split(/[\\/]/).pop() ?? "").toLowerCase() + return shellPath.split(/[\\/]/).pop() ?? "" +} + +export function describeShellDialect(shellPath: string): string | undefined { + const name = getShellName(shellPath).toLowerCase() if (name === "cmd.exe" || name === "cmd") { return (