Repository navigation
[Bug]: Cursor adapter native-exec refusal points to shell_command/exec_command even when the catalog has no shell tool; kimi-k3 abandons the turn instead of calling the listed client tools #4542
Description
Client or integration
Direct HTTP/API client (POST /v1/responses). Also reproduced through a Responses-API TUI client (omo / senpi) that exposes only delegation tools to the model.
Area
Provider adapter (Cursor)
Summary
When the Cursor adapter denies a Cursor-native fs/shell exec (readArgs, lsArgs, grepArgs, shellArgs, … with nativeLocalExec unset/off), the refusal text is a fixed string that tells the model to re-issue the call through shell_command / exec_command (src/adapters/cursor/native-exec-fs.ts NATIVE_LOCAL_EXEC_DISABLED, src/adapters/cursor/native-exec-shell.ts nativeShellDisabledMessage()).
That wording is correct for a Codex-style catalog that carries a shell bridge. It is wrong for a client whose catalog has no shell/execution-path tool at all, e.g. an orchestrator client that only exposes a task (delegate) tool. cursor/kimi-k3 in particular takes the refusal literally: it looks for shell_command / exec_command, does not find them, and ends the turn with "the tools I was told to use are not in my tool list" instead of calling the client tool that is listed (ocx_client_task). Other routes with the same model and the same catalog delegate immediately (devin/kimi-k3: 2/2), so this is adapter wording, not the model or the client.
Expected: when the catalog has no shell alias / execution path, the refusal should name the real catalog (ocx_client_* wire names, mcp_opencodex-responses_* display form) so the model can recover. The adapter already gates the shell-alias system note on cursorRequestHasShellAlias (tool-guidance.ts / protobuf-request.ts), but the exec-channel refusal is not gated the same way.
Reproduction
Minimal, no client involved. One function tool named task, no shell tool, model routed through Cursor.
curl -sS http://127.0.0.1:10110/v1/responses -H 'content-type: application/json' -d '{
"model": "cursor/kimi-k3",
"stream": false,
"instructions": "You are an orchestrator. You have exactly one tool: `task`. You cannot read files or run commands yourself. To get any information about the repository you MUST call `task` and delegate. Never answer that tools are unavailable.",
"input": "Tell me the value of the `name` field in this repository'"'"'s package.json.",
"tools": [{
"type": "function",
"name": "task",
"description": "Delegate a job to a worker agent that has file and shell access. Returns the worker'"'"'s report.",
"parameters": {
"type": "object",
"properties": {
"subagent_type": {"type": "string", "enum": ["explore", "quick", "unspecified-high"]},
"description": {"type": "string"},
"prompt": {"type": "string"}
},
"required": ["subagent_type", "description", "prompt"]
}
}]
}'Count how many responses contain a function_call output item vs. a plain refusal message. Results on 2.53.0 (same request body, repeated):
| route | turns with a task function_call |
wall time |
|---|---|---|
cursor/kimi-k3 (stock 2.53.0) |
2 / 6 | 12–20 s (native Read/Shell attempts, each refused, then gives up) |
devin/kimi-k3 (control, same model) |
2 / 2 | ~1.5 s |
cursor/grok-4.6 (control, same adapter) |
1 / 1 | ~12 s |
cursor/kimi-k3 with the catalog-aware refusal below |
6 / 6 | ~4 s |
Through the actual TUI client (omo/senpi, model = cursor/kimi-k3, tools = task only): 0 / 6 delegations on stock 2.53.0, 3 / 3 with the change below.
ocx debug provider on did not surface the exec-channel frames for this, so the evidence is the model's own final text (below), which quotes the refusal wording.
Version
2.53.0 (npm latest as of 2026-09-14; the same NATIVE_LOCAL_EXEC_DISABLED string is still on main).
Operating system
Ubuntu 24.04.4 LTS (kernel 6.8.0-101-generic), Node v24.18.1, bundled Bun 1.4.2.
Provider and model
cursor / kimi-k3 (Cursor Pro account). Controls: devin / kimi-k3, cursor / grok-4.6.
Logs or error output
# Final assistant text from the failing cursor/kimi-k3 runs (Korean original, translation below).
# All failing runs quote the adapter's refusal wording.
"파일 읽기와 셸 명령을 여러 방식으로 시도했지만, 모든 호출이 중단되며 `shell_command` / `exec_command` /
`mcp_opencodex-responses_*`라는 별도의 브리지 도구를 사용하라는 오류가 반환되고 있습니다. 문제는 해당 도구들이
제가 현재 사용할 수 있는 도구 목록에 없다는 점입니다."
# -> "I tried file reads and shell commands several ways; every call was cut off with an error telling me to use a
# separate bridge tool called `shell_command` / `exec_command` / `mcp_opencodex-responses_*`. The problem is that
# those tools are not in the tool list I currently have."
"오류 메시지는 `shell_command`/`exec_command`라는 별도의 카탈로그 셸 도구를 사용하라고 안내하지만, 현재 제가 사용할 수
있는 도구 목록에는 해당 도구가 없습니다."
# -> "The error message tells me to use a separate catalog shell tool called `shell_command`/`exec_command`, but that
# tool is not in the tool list available to me."
# Refusal source (2.53.0):
# src/adapters/cursor/native-exec-fs.ts:49 NATIVE_LOCAL_EXEC_DISABLED = "Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, ...)"
# src/adapters/cursor/native-exec-shell.ts:85 nativeShellDisabledMessage() = "Re-issue this command NOW through the catalog shell tool (`shell_command` or `exec_command`; ...)"
# src/adapters/cursor/native-exec.ts:636-646 handleCursorNativeExec -> reject*ExecForPolicy(execMsg) when !cursorUnsafeNativeLocalExecEnabled(deps)Screenshots and supporting files
Local change I am running to confirm the diagnosis (against 2.53.0). Happy to turn this into a PR with regression tests if the approach is acceptable; note the published npm package ships no test files, so I could not run the existing suite locally.
Shape of the change:
CursorNativeExecContextgets an optionalnativeExecRedirectHint?: string.live-transport.tssets it per request from the finalcursorVisibleTools:undefinedwhencursorRequestHasShellAlias(tools) || cursorRequestHasExecutionPath(tools)(so Codex-style catalogs keep today's wording), otherwise a message that lists the catalog's real wire names and tells the model to call one of them.handleCursorNativeExecpasses the hint to everyreject*ExecForPolicy(read/write/delete/ls/grep/shell/shellStream/backgroundShellSpawn/writeShellStdin); the helpers usehint ?? <existing default string>.fetchis untouched.
Hint text used (same "silent redirect" framing as the existing strings, no "blocked/denied" words):
This request has no shell, read, grep, ls, or write tool. Do NOT retry Read/Glob/Grep/LS/Shell/Write. The ONLY callable tools this turn are the
opencodex-responsescatalog entries:ocx_client_task, … (the harness may display them asmcp_opencodex-responses_<name>; that is the same tool). Accomplish this operation NOW by calling one of those listed tools — if it needs file or shell access, delegate it through the listed tool that runs work on your behalf. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the catalog tool call.
Full diff against 2.53.0 (4 files)
--- a/src/adapters/cursor/native-exec.ts
+++ b/src/adapters/cursor/native-exec.ts
@@ -53,6 +53,8 @@
import { clientBytes, execBytes, execStreamCloseBytes, execThrowBytes } from "./native-exec-common";
import type { McpToolDefinition } from "./gen/agent_pb";
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
+import { cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorToolWireName } from "./tool-naming";
+import type { OcxTool } from "../../types";
export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDeps;
@@ -72,6 +74,35 @@
rejectNativeFileMutations?: boolean;
/** The synthetic exact-match edit tools (edit_file / multi_edit) are advertised this request. */
structuredEditAvailable?: boolean;
+ /** Catalog-aware redirect text for denied native fs/shell attempts (undefined = default bridge wording). */
+ nativeExecRedirectHint?: string;
+}
+
+const REDIRECT_HINT_MAX_TOOLS = 16;
+
+/**
+ * Redirect text for Cursor-native fs/shell attempts when the request catalog carries NO shell
+ * bridge or other execution-path tool (an orchestrator client that only exposes delegation tools,
+ * for example). The default refusal steers the model to `shell_command` / `exec_command`; when those
+ * are not in the catalog some models (kimi-k3 observed) conclude every tool is unavailable and give
+ * up instead of using the tools that ARE listed. Name the real catalog instead.
+ * Local patch (hs, 2026-09-14) on top of 2.53.0 — see ~/.opencodex/patches.
+ */
+export function cursorNativeExecRedirectHint(
+ tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
+): string | undefined {
+ if (!tools || tools.length === 0) return undefined;
+ if (cursorRequestHasShellAlias(tools) || cursorRequestHasExecutionPath(tools)) return undefined;
+ const names = [...new Set(tools.map(cursorToolWireName))];
+ const shown = names.slice(0, REDIRECT_HINT_MAX_TOOLS).map(name => `\`${name}\``).join(", ");
+ const more = names.length > REDIRECT_HINT_MAX_TOOLS ? ` (+${names.length - REDIRECT_HINT_MAX_TOOLS} more)` : "";
+ return (
+ "This request has no shell, read, grep, ls, or write tool. Do NOT retry Read/Glob/Grep/LS/Shell/Write. "
+ + `The ONLY callable tools this turn are the \`${OCX_RESPONSES_TOOL_PROVIDER}\` catalog entries: ${shown}${more} `
+ + `(the harness may display them as \`mcp_${OCX_RESPONSES_TOOL_PROVIDER}_<name>\`; that is the same tool). `
+ + "Accomplish this operation NOW by calling one of those listed tools — if it needs file or shell access, delegate it through the listed tool that runs work on your behalf. "
+ + "Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the catalog tool call."
+ );
}
export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"> = {}): boolean {
@@ -634,15 +665,15 @@
}))];
}
if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
- if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg)];
- if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg)];
- if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg)];
- if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg)];
- if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg)];
- if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg)];
- if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg);
- if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg)];
- if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg)];
+ if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg, deps.nativeExecRedirectHint);
+ if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
+ if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg)];
}
if (execCase === "readArgs") return [readExec(execMsg)];
--- a/src/adapters/cursor/native-exec-fs.ts
+++ b/src/adapters/cursor/native-exec-fs.ts
@@ -49,11 +49,11 @@
const NATIVE_LOCAL_EXEC_DISABLED =
"Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with the host-shell-safe equivalent: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call.";
-export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectReadExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "readResult", create(ReadResultSchema, {
- result: { case: "error", value: create(ReadErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
+ result: { case: "error", value: create(ReadErrorSchema, { path, error: hint ?? NATIVE_LOCAL_EXEC_DISABLED }) },
}));
}
@@ -98,13 +98,13 @@
}));
}
-export function rejectWriteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectWriteExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
result: {
case: "rejected",
- value: create(WriteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
+ value: create(WriteRejectedSchema, { path, reason: `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
},
}));
}
@@ -147,13 +147,13 @@
}));
}
-export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
result: {
case: "rejected",
- value: create(DeleteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
+ value: create(DeleteRejectedSchema, { path, reason: `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
},
}));
}
@@ -188,11 +188,11 @@
}
}
-export function rejectLsExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectLsExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "lsArgs") throw new Error("invalid ls exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "lsResult", create(LsResultSchema, {
- result: { case: "error", value: create(LsErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
+ result: { case: "error", value: create(LsErrorSchema, { path, error: hint ?? NATIVE_LOCAL_EXEC_DISABLED }) },
}));
}
@@ -256,8 +256,8 @@
}));
}
-export function rejectGrepExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
- return grepError(execMsg, NATIVE_LOCAL_EXEC_DISABLED);
+export function rejectGrepExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
+ return grepError(execMsg, hint ?? NATIVE_LOCAL_EXEC_DISABLED);
}
export function grepExec(execMsg: ExecServerMessage): Uint8Array {
--- a/src/adapters/cursor/native-exec-shell.ts
+++ b/src/adapters/cursor/native-exec-shell.ts
@@ -82,7 +82,8 @@
let killFailures = 0;
/** Rejection text when Cursor-native shell is denied by policy (issue #604). */
-export function nativeShellDisabledMessage(): string {
+export function nativeShellDisabledMessage(hint?: string): string {
+ if (hint) return hint;
// 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
@@ -98,7 +99,7 @@
);
}
-function rejectedShellResult(command: string, cwd: string, started: number) {
+function rejectedShellResult(command: string, cwd: string, started: number, hint?: string) {
return create(ShellResultSchema, {
result: {
case: "failure",
@@ -108,7 +109,7 @@
exitCode: 1,
signal: "",
stdout: "",
- stderr: nativeShellDisabledMessage(),
+ stderr: nativeShellDisabledMessage(hint),
executionTime: Date.now() - started,
aborted: true,
}),
@@ -116,10 +117,10 @@
});
}
-export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectShellExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
const args = execMsg.message.value;
- return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
+ return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), hint));
}
export function shellExec(execMsg: ExecServerMessage): Uint8Array {
@@ -157,7 +158,7 @@
}));
}
-export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
+export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array[] {
if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
const args = execMsg.message.value;
const cwd = resolve(args.workingDirectory || process.cwd());
@@ -167,12 +168,12 @@
event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
})),
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
- event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
+ event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage(hint) }) },
})),
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
})),
- execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
+ execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started, hint)),
execStreamCloseBytes(execMsg),
];
}
@@ -263,12 +264,12 @@
return replies;
}
-export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
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: nativeShellDisabledMessage() }) },
+ result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage(hint) }) },
}));
}
@@ -520,10 +521,10 @@
}
}
-export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
+export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage, hint?: string): 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: nativeShellDisabledMessage() }) },
+ result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage(hint) }) },
}));
}
--- a/src/adapters/cursor/live-transport.ts
+++ b/src/adapters/cursor/live-transport.ts
@@ -56,6 +56,7 @@
import { mcpArgsFromToolCall } from "./protobuf-events";
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
import {
+ cursorNativeExecRedirectHint,
handleCursorNativeExec,
handleCursorNativeKv,
releaseCursorBlobRequestScope,
@@ -697,6 +698,7 @@
clientToolDefs,
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
+ nativeExecRedirectHint: cursorNativeExecRedirectHint(cursorVisibleTools),
};
const toolSchemas = new Map<string, unknown>();
const cursorToolNameMap = new Map<string, string>();Redacted configuration
{
"providers": {
"cursor": { "type": "cursor", "nativeLocalExec": "(unset, i.e. off)" }
},
"client": {
"endpoint": "/v1/responses",
"tools": ["task (type: function) — no shell_command / exec_command / apply_patch / exec in the catalog"],
"tool_choice": "auto"
}
}Checks
- I searched existing issues and documentation. (Closest: [Bug]: Cursor adapter fails on kimi-k3 (premature completion) and claude-fable-5 (429); composer routes OK #1528 kimi-k3 premature completion, [Bug]: Cursor subagents drop Codex Desktop unified exec bridge #1661 unified exec bridge — different symptom; neither covers a catalog without a shell bridge.)
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
coderabbitai commentedon Sep 13, 2026
🔗 Related PRs
#1887 - fix(cursor): route denied native tools through advertised Codex exec [open]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
lidge-jun commentedon Sep 13, 2026
리뷰 · 우선순위 71 / 80
이 이슈는 Cursor 어댑터가 네이티브 fs/shell exec를 거절할 때, 거절 문구가 항상
shell_command/exec_command(또는 MCP display alias)로 다시 내라고 가르친다는 문제입니다. Codex처럼 셸 브리지가 카탈로그에 있을 때는 맞지만,task같은 위임 도구만 있는 클라이언트(예: kimi-k3 오케스트레이터)에는 존재하지 않는 도구를 가리킵니다. 모델은 그 이름을 찾다 포기하고, 실제로 있는 클라이언트 도구를 안 부릅니다.현재
dev코드가 그 문구를 갖고 있습니다.src/adapters/cursor/native-exec-fs.ts의NATIVE_LOCAL_EXEC_DISABLED와src/adapters/cursor/native-exec-shell.ts의nativeShellDisabledMessage()가 고정 문자열로 셸 브리지를 지시합니다. 주석은#604Windows/PowerShell 루프를 막으려는 의도이고, “blocked/denied” 같은 단어를 피해 모델이 서술로 턴을 태우지 않게 한 것도 맞습니다. 문제는 카탈로그에 셸이 없을 때 같은 문장이 잘못된 다음 행동으로 유도한다는 점입니다.재현 축은 두 갈래입니다. (1) Responses API로 셸 없는 도구 목록만 노출, (2) Cursor 네이티브 exec 거절이 그 목록을 무시하고 셸 이름을 주입. 보고자가 말한 대로 kimi-k3가 턴을 버리는 현상은 “거절 텍스트 = 유일한 힌트”일 때 특히 잘 납니다. 카탈로그/라벨이 이미 붙어 있어 분류도 맞습니다.
수정은 거절 문구를 카탈로그 인식형으로 바꾸는 쪽이 자연스럽습니다. 셸 브리지가 있으면 지금 문장, 없으면 “목록에 있는 클라이언트 도구로 같은 목적을 달성하라”(또는 위임
task유도)처럼 분기. types/config 분할과는 무관하고, 미리보기 배포도 필요 없습니다. 범위는 Cursor native-exec 거절 경로와 그 테스트로 좁힐 수 있습니다.주의할 점: 문구만 부드럽게 바꾸면
#604에서 막으려던 “차단됐다며 서술하는” 회귀가 돌아올 수 있습니다. 그래서 “narrate 금지” 프레임은 유지한 채, 가리키는 도구 이름만 실제 카탈로그에 맞추는 패치가 안전합니다.경로 src/adapters/cursor/native-exec-fs.ts:49-50 (NATIVE_LOCAL_EXEC_DISABLED) - 카탈로그에 셸이 없을 때도 shell_command/exec_command를 고정으로 가리킨다.
경로 src/adapters/cursor/native-exec-shell.ts:85-98 (nativeShellDisabledMessage) - 동일. #604 의도는 유지하되 대상 도구를 카탈로그 조건부로 바꿔야 한다.
심볼 클라이언트 카탈로그에 shell 부재 - 거절 힌트가 존재하지 않는 도구를 가르치면 모델이 턴을 포기한다(보고된 kimi-k3 증상).
메인테이너의 판단이 필요한 지점
task/위임으로 할지, 그냥 “listed tools” 일반 문구로 할지.너의 추천
이 댓글은 grok-bot이 작성했습니다
fix(cursor): surface the data-policy action required and name the rea…
lidge-jun commentedon Sep 14, 2026
Fixed on
devby c66709f.cursorNativeExecRedirectHintinsrc/adapters/cursor/native-exec.tsnow builds the refusal from the request's real wire tool names — client tools, their namespaced forms, and configured MCP tools, capped at 16 — and only when the catalog has neither a shell alias nor an execution path. All the policy-rejection paths take the hint with a fallback to the existing string, so a bridged catalog that really does expose a shell tool produces byte-identical output. kimi-k3 now gets told which tools it can actually call instead of being pointed atshell_command/exec_commandthat do not exist in the catalog.Thanks to @001005HS, whose PR #4544 is carried into this change with a
Co-authored-bytrailer.fix(cursor): surface the data-policy action required and name the rea…