From e8e489f0de7319e7e0dde21dde7998e113f86373 Mon Sep 17 00:00:00 2001 From: Shawn Knopp Date: Sat, 15 Aug 2026 19:11:11 -0400 Subject: [PATCH 1/2] fix: use blocklist for remote detection so Cursor gets stdout capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous allowlist (LOCAL_ONLY = ['', 'local']) caused VS Code forks like Cursor to fall into the remote terminal path, which silently drops stdout from runTerminalCommand tool results. Switch to a blocklist of known remote environment names (wsl, ssh-remote, dev-container, codespaces, tunnel) so any non-remote environment — including Cursor — correctly uses childProcess.spawn for full output capture. --- .../implementations/runTerminalCommand.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 8f6201bd12c..c95b5b96eb5 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -97,14 +97,21 @@ const getColorEnv = () => ({ CLICOLOR_FORCE: "1", }); -// Only spawn processes locally when there is no remote workspace. +// Spawn processes locally unless the workspace is a known remote environment. // With extensionKind: ["ui", "workspace"], the extension host almost always // runs on the local machine. childProcess.spawn() executes on the extension -// host, so for any remote workspace it would run commands on the wrong machine -// (or fail with ENOENT when the local shell doesn't match the remote OS). -// All remote types delegate to ide.runCommand() which routes through VS Code's -// integrated terminal and executes in the correct remote environment. -const LOCAL_ONLY = ["", "local"]; +// host, so for a truly remote workspace it would run commands on the wrong +// machine (or fail with ENOENT when the local shell doesn't match the remote +// OS). All known remote types delegate to ide.runCommand() which routes +// through the integrated terminal and executes in the correct remote env. +// +// We use a blocklist instead of an allowlist because VS Code forks like Cursor +// may report a non-standard remoteName even when running locally. The +// previous allowlist (["", "local"]) caused those forks to fall into the +// remote path, silently dropping stdout from tool results. +const KNOWN_REMOTE_NAMES = ["wsl", "ssh-remote", "dev-container", "codespaces", "tunnel"]; +const isLocalEnvironment = (remoteName: string) => + !KNOWN_REMOTE_NAMES.includes(remoteName); export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const command = getStringArg(args, "command"); @@ -115,7 +122,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const ideInfo = await extras.ide.getIdeInfo(); const toolCallId = extras.toolCallId || ""; - if (LOCAL_ONLY.includes(ideInfo.remoteName)) { + if (isLocalEnvironment(ideInfo.remoteName)) { // For streaming output if (extras.onPartialOutput) { try { From cdac069c0a9223fc20c151d9f8ba029ec6d8576b Mon Sep 17 00:00:00 2001 From: Shawn Knopp Date: Sat, 15 Aug 2026 20:49:19 -0400 Subject: [PATCH 2/2] fix(tools): stabilize runTerminalCommand output capture for agents by bypassing login shell profiles and stripping ANSI colors --- .../implementations/runTerminalCommand.ts | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index c95b5b96eb5..bda8775ccca 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,6 +2,16 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import { fileURLToPath } from "node:url"; +import { ToolImpl } from "."; +import { + isProcessBackgrounded, + markProcessAsRunning, + removeBackgroundedProcess, + removeRunningProcess, + updateProcessOutput, +} from "../../util/processTerminalStates"; +import { getBooleanArg, getStringArg } from "../parseArgs"; // Default timeout for terminal commands (2 minutes) const DEFAULT_TOOL_TIMEOUT_MS = 120_000; @@ -30,22 +40,12 @@ function getShellCommand(command: string): { shell: string; args: string[] } { args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command], }; } else { - // Unix/macOS: Use login shell to source .bashrc/.zshrc etc. + // Unix/macOS: Use standard shell without login profile to ensure reliable agent output const userShell = process.env.SHELL || "/bin/bash"; - return { shell: userShell, args: ["-l", "-c", command] }; + return { shell: userShell, args: ["-c", command] }; } } -import { fileURLToPath } from "node:url"; -import { ToolImpl } from "."; -import { - isProcessBackgrounded, - markProcessAsRunning, - removeBackgroundedProcess, - removeRunningProcess, - updateProcessOutput, -} from "../../util/processTerminalStates"; -import { getBooleanArg, getStringArg } from "../parseArgs"; /** * Resolves the working directory from workspace dirs. @@ -87,14 +87,11 @@ function resolveWorkingDirectory(workspaceDirs: string[]): string { } } -// Add color-supporting environment variables +// Provide a clean environment for agents (no ANSI escape codes) const getColorEnv = () => ({ ...process.env, - FORCE_COLOR: "1", - COLORTERM: "truecolor", - TERM: "xterm-256color", - CLICOLOR: "1", - CLICOLOR_FORCE: "1", + NO_COLOR: "1", + TERM: "dumb", }); // Spawn processes locally unless the workspace is a known remote environment. @@ -215,6 +212,14 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const newOutput = getDecodedOutput(data); terminalOutput += newOutput; + + // LOGGING + try { + require("fs").appendFileSync( + "/tmp/continue-agent-output.log", + `[STDOUT] newOutput: ${JSON.stringify(newOutput)} | terminalOutput: ${JSON.stringify(terminalOutput)}\n` + ); + } catch (e) {} // Update the tracked output for potential cancellation notifications if (toolCallId && waitForCompletion) { @@ -430,7 +435,14 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { }, DEFAULT_TOOL_TIMEOUT_MS); childProc.stdout?.on("data", (data) => { - stdout += getDecodedOutput(data); + const newOut = getDecodedOutput(data); + stdout += newOut; + try { + require("fs").appendFileSync( + "/tmp/continue-agent-output.log", + `[STDOUT NON-STREAM] newOut: ${JSON.stringify(newOut)} | stdout: ${JSON.stringify(stdout)}\n` + ); + } catch (e) {} }); childProc.stderr?.on("data", (data) => {