From 2611629ed27fc03b7bfc507de10f0aca45e21029 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:18:39 +0200 Subject: [PATCH 1/3] fix(cursor): steer Windows bridge shell away from PS 5.1 syntax loops Native-shell rejection told the model to replay the same bash/CMD command through the Codex bridge, which on Windows PowerShell 5.1 fails and loops (#604). Adapt for the host shell and inject PS-safe guidance instead. --- src/adapters/cursor/native-exec-shell.ts | 23 ++++++++++++++----- src/adapters/cursor/tool-definitions.ts | 15 +++++++++--- tests/cursor-native-exec-policy.test.ts | 2 ++ tests/cursor-tool-definitions.test.ts | 29 ++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/adapters/cursor/native-exec-shell.ts b/src/adapters/cursor/native-exec-shell.ts index 27c07fd71..013c96785 100644 --- a/src/adapters/cursor/native-exec-shell.ts +++ b/src/adapters/cursor/native-exec-shell.ts @@ -23,8 +23,19 @@ import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common const backgroundShells = new Map(); let nextShellId = 1; -const NATIVE_SHELL_DISABLED = - "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool with the same command."; +function nativeShellDisabledMessage(): string { + // Do not insist on "the same command" — on Windows PowerShell 5.1 that steers the + // model into replaying bash/CMD idioms (`cd /d`, `&&`, heredocs) through the bridge + // and looping (issue #604). + const adapt = process.platform === "win32" + ? "Adapt the command for Windows PowerShell 5.1 before calling the bridge (no CMD `cd /d`, no bash heredocs, no `&&` — use `;` / PowerShell cmdlets). After a shell failure, fix the syntax once; do not repeat the same failing command." + : "Adapt the command for the host shell if needed before calling the bridge. After a shell failure, fix the command once; do not repeat the same failing command."; + return ( + "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). " + + "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. " + + adapt + ); +} function rejectedShellResult(command: string, cwd: string, started: number) { return create(ShellResultSchema, { @@ -36,7 +47,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) { exitCode: 1, signal: "", stdout: "", - stderr: NATIVE_SHELL_DISABLED, + stderr: nativeShellDisabledMessage(), executionTime: Date.now() - started, aborted: true, }), @@ -95,7 +106,7 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) }, })), execBytes(execMsg, "shellStream", create(ShellStreamSchema, { - event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: NATIVE_SHELL_DISABLED }) }, + event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) }, })), execBytes(execMsg, "shellStream", create(ShellStreamSchema, { event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) }, @@ -196,7 +207,7 @@ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessa const args = execMsg.message.value; const cwd = resolve(args.workingDirectory || process.cwd()); return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, { - result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: NATIVE_SHELL_DISABLED }) }, + result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) }, })); } @@ -233,7 +244,7 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array { if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec"); return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, { - result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: NATIVE_SHELL_DISABLED }) }, + result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) }, })); } diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 274cda914..193fd2b82 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -401,8 +401,10 @@ function discoveryToolLabel(wireNames: readonly string[]): string | undefined { export function buildCursorToolGuidanceSystemNote( tools: readonly Pick[] | undefined, toolChoice?: OcxRequestOptions["toolChoice"], + options?: { platform?: NodeJS.Platform }, ): string | undefined { if (!tools?.length) return undefined; + const platform = options?.platform ?? process.platform; const wireNames = [...new Set( tools .filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools)) @@ -417,6 +419,14 @@ export function buildCursorToolGuidanceSystemNote( const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice); const discoveryTools = discoveryToolLabel(wireNames); const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames); + const windowsShellNote = hasBareExec && platform === "win32" + ? "Host shell is Windows PowerShell 5.1-compatible: use PowerShell syntax only. Do not use CMD `cd /d`, bash heredocs (`< typeof note === "string"); return notes.join(" "); } diff --git a/tests/cursor-native-exec-policy.test.ts b/tests/cursor-native-exec-policy.test.ts index e745cdec3..1814b2222 100644 --- a/tests/cursor-native-exec-policy.test.ts +++ b/tests/cursor-native-exec-policy.test.ts @@ -142,6 +142,8 @@ describe("Cursor native exec sandbox policy", () => { expect(deniedShellText).toContain("exec_command"); expect(deniedShellText).toContain("mcp_opencodex-responses_*"); expect(deniedShellText).toContain("Do not tell the user"); + expect(deniedShellText).not.toContain("with the same command"); + expect(deniedShellText).toContain("do not repeat the same failing command"); expect(deniedShellText).not.toContain("disabled by OpenCodex policy"); expect(deniedShellText).not.toContain("sandbox denial"); expect(deniedShell.message.case).toBe("shellResult"); diff --git a/tests/cursor-tool-definitions.test.ts b/tests/cursor-tool-definitions.test.ts index 652a93515..75763b92f 100644 --- a/tests/cursor-tool-definitions.test.ts +++ b/tests/cursor-tool-definitions.test.ts @@ -324,6 +324,35 @@ describe("Cursor tool definitions", () => { expect(note).toContain("Never tell the user that shell or read access is blocked"); }); + test("adds Windows PowerShell 5.1 host-shell guidance when platform is win32 (#604)", () => { + const note = buildCursorToolGuidanceSystemNote( + [{ name: "shell_command", description: "Run", parameters: {} }], + undefined, + { platform: "win32" }, + ); + expect(note).toBeDefined(); + if (!note) throw new Error("Expected Cursor tool guidance note"); + + expect(note).toContain("Windows PowerShell 5.1"); + expect(note).toContain("cd /d"); + expect(note).toContain("< { + const note = buildCursorToolGuidanceSystemNote( + [{ name: "exec_command", description: "Run", parameters: {} }], + undefined, + { platform: "linux" }, + ); + expect(note).toBeDefined(); + if (!note) throw new Error("Expected Cursor tool guidance note"); + expect(note).not.toContain("Windows PowerShell 5.1"); + expect(note).toContain("`cat`, `ls`, `rg`, `grep`"); + }); + test("adds codex-native edit guidance only when apply_patch is advertised", () => { const tools: OcxTool[] = [ { name: "exec_command", description: "Run", parameters: {} }, From b5d5bc529cccdaed2886e48c9a384110c68cb1af Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:01:17 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(cursor):=20address=20#604=20review=20?= =?UTF-8?q?=E2=80=94=20host-shell-neutral=20retry-stop=20guidance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop proxy-OS platform branching and `;`-as-`&&` advice; use if ($?) and one corrected bridge attempt for Codex/CodeRabbit feedback on #627. --- src/adapters/cursor/native-exec-shell.ts | 17 ++++++++------- src/adapters/cursor/tool-definitions.ts | 19 +++++++---------- tests/cursor-native-exec-policy.test.ts | 5 ++++- tests/cursor-tool-definitions.test.ts | 27 ++++++------------------ 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/adapters/cursor/native-exec-shell.ts b/src/adapters/cursor/native-exec-shell.ts index 013c96785..4b9effa17 100644 --- a/src/adapters/cursor/native-exec-shell.ts +++ b/src/adapters/cursor/native-exec-shell.ts @@ -23,17 +23,18 @@ import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common const backgroundShells = new Map(); let nextShellId = 1; -function nativeShellDisabledMessage(): string { - // Do not insist on "the same command" — on Windows PowerShell 5.1 that steers the - // model into replaying bash/CMD idioms (`cd /d`, `&&`, heredocs) through the bridge - // and looping (issue #604). - const adapt = process.platform === "win32" - ? "Adapt the command for Windows PowerShell 5.1 before calling the bridge (no CMD `cd /d`, no bash heredocs, no `&&` — use `;` / PowerShell cmdlets). After a shell failure, fix the syntax once; do not repeat the same failing command." - : "Adapt the command for the host shell if needed before calling the bridge. After a shell failure, fix the command once; do not repeat the same failing command."; +/** Rejection text when Cursor-native shell is denied by policy (issue #604). */ +export function nativeShellDisabledMessage(): string { + // Do not insist on "the same command" — that steers models into replaying bash/CMD + // idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604). + // Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex + // client that executes the bridge (LAN/SSH remote-proxy). return ( "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). " + "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. " - + adapt + + "Adapt the command for the Codex client host shell before calling the bridge " + + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` when a later step must run only after success — do not treat `;` as a substitute for `&&`). " + + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands." ); } diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 193fd2b82..94b10f39c 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -401,10 +401,8 @@ function discoveryToolLabel(wireNames: readonly string[]): string | undefined { export function buildCursorToolGuidanceSystemNote( tools: readonly Pick[] | undefined, toolChoice?: OcxRequestOptions["toolChoice"], - options?: { platform?: NodeJS.Platform }, ): string | undefined { if (!tools?.length) return undefined; - const platform = options?.platform ?? process.platform; const wireNames = [...new Set( tools .filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools)) @@ -419,13 +417,10 @@ export function buildCursorToolGuidanceSystemNote( const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice); const discoveryTools = discoveryToolLabel(wireNames); const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames); - const windowsShellNote = hasBareExec && platform === "win32" - ? "Host shell is Windows PowerShell 5.1-compatible: use PowerShell syntax only. Do not use CMD `cd /d`, bash heredocs (`< typeof note === "string"); return notes.join(" "); } diff --git a/tests/cursor-native-exec-policy.test.ts b/tests/cursor-native-exec-policy.test.ts index 1814b2222..e89d31472 100644 --- a/tests/cursor-native-exec-policy.test.ts +++ b/tests/cursor-native-exec-policy.test.ts @@ -143,7 +143,10 @@ describe("Cursor native exec sandbox policy", () => { expect(deniedShellText).toContain("mcp_opencodex-responses_*"); expect(deniedShellText).toContain("Do not tell the user"); expect(deniedShellText).not.toContain("with the same command"); - expect(deniedShellText).toContain("do not repeat the same failing command"); + expect(deniedShellText).toContain("at most one corrected bridge attempt"); + expect(deniedShellText).toContain("if ($?)"); + expect(deniedShellText).toContain("do not treat `;` as a substitute for `&&`"); + expect(deniedShellText).toContain("Windows PowerShell 5.1"); expect(deniedShellText).not.toContain("disabled by OpenCodex policy"); expect(deniedShellText).not.toContain("sandbox denial"); expect(deniedShell.message.case).toBe("shellResult"); diff --git a/tests/cursor-tool-definitions.test.ts b/tests/cursor-tool-definitions.test.ts index 75763b92f..655a39b0c 100644 --- a/tests/cursor-tool-definitions.test.ts +++ b/tests/cursor-tool-definitions.test.ts @@ -324,33 +324,20 @@ describe("Cursor tool definitions", () => { expect(note).toContain("Never tell the user that shell or read access is blocked"); }); - test("adds Windows PowerShell 5.1 host-shell guidance when platform is win32 (#604)", () => { - const note = buildCursorToolGuidanceSystemNote( - [{ name: "shell_command", description: "Run", parameters: {} }], - undefined, - { platform: "win32" }, - ); + test("adds host-shell-neutral PowerShell and one-retry-stop guidance (#604)", () => { + const note = buildCursorToolGuidanceSystemNote([{ name: "shell_command", description: "Run", parameters: {} }]); expect(note).toBeDefined(); if (!note) throw new Error("Expected Cursor tool guidance note"); expect(note).toContain("Windows PowerShell 5.1"); expect(note).toContain("cd /d"); expect(note).toContain("< { - const note = buildCursorToolGuidanceSystemNote( - [{ name: "exec_command", description: "Run", parameters: {} }], - undefined, - { platform: "linux" }, - ); - expect(note).toBeDefined(); - if (!note) throw new Error("Expected Cursor tool guidance note"); - expect(note).not.toContain("Windows PowerShell 5.1"); - expect(note).toContain("`cat`, `ls`, `rg`, `grep`"); + expect(note).toContain("`cat`/`ls`/`rg`"); + expect(note).toContain("Codex client host"); }); test("adds codex-native edit guidance only when apply_patch is advertised", () => { From bacda33e06d75bf45bbad0b7924dfeb17ba247a7 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:20:17 +0200 Subject: [PATCH 3/3] fix(cursor): state PowerShell 5.1 has no &&/|| in #604 guidance CodeRabbit follow-up: make unsupported-parser-error wording explicit and assert it in rejection/guidance tests. --- src/adapters/cursor/native-exec-shell.ts | 2 +- src/adapters/cursor/tool-definitions.ts | 2 +- tests/cursor-native-exec-policy.test.ts | 1 + tests/cursor-tool-definitions.test.ts | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/adapters/cursor/native-exec-shell.ts b/src/adapters/cursor/native-exec-shell.ts index 4b9effa17..cda058fca 100644 --- a/src/adapters/cursor/native-exec-shell.ts +++ b/src/adapters/cursor/native-exec-shell.ts @@ -33,7 +33,7 @@ export function nativeShellDisabledMessage(): string { "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). " + "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. " + "Adapt the command for the Codex client host shell before calling the bridge " - + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` when a later step must run only after success — do not treat `;` as a substitute for `&&`). " + + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). " + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands." ); } diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 94b10f39c..7688f209a 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -420,7 +420,7 @@ export function buildCursorToolGuidanceSystemNote( // Host-shell-neutral: the Codex client executes bridge commands, and may differ from // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls. const hostShellNote = hasBareExec - ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`< { expect(deniedShellText).not.toContain("with the same command"); expect(deniedShellText).toContain("at most one corrected bridge attempt"); expect(deniedShellText).toContain("if ($?)"); + expect(deniedShellText).toContain("`&&`/`||` are unsupported parser errors"); expect(deniedShellText).toContain("do not treat `;` as a substitute for `&&`"); expect(deniedShellText).toContain("Windows PowerShell 5.1"); expect(deniedShellText).not.toContain("disabled by OpenCodex policy"); diff --git a/tests/cursor-tool-definitions.test.ts b/tests/cursor-tool-definitions.test.ts index 655a39b0c..d418b304b 100644 --- a/tests/cursor-tool-definitions.test.ts +++ b/tests/cursor-tool-definitions.test.ts @@ -333,6 +333,7 @@ describe("Cursor tool definitions", () => { expect(note).toContain("cd /d"); expect(note).toContain("<