From ab0d8ef8c10d5f78f3346d8baac80fed2675e893 Mon Sep 17 00:00:00 2001 From: Artale <117890364+arosstale@users.noreply.github.com> Date: Fri, 13 Feb 2026 19:27:06 +0100 Subject: [PATCH] fix(daemon): preserve backslashes in parseCommandLine on Windows (#15642) * fix(daemon): preserve backslashes in parseCommandLine on Windows Only treat backslash as escape when followed by a quote or another backslash. Bare backslashes are kept as-is so Windows paths survive. Fixes #15587 * fix(daemon): preserve UNC backslashes in schtasks parsing (#15642) (thanks @arosstale) --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + src/daemon/schtasks.test.ts | 59 +++++++++++++++++++++++++++++++++++++ src/daemon/schtasks.ts | 16 +++++----- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49ca6117ce..a859cb5740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Agents/Image tool: cap image-analysis completion `maxTokens` by model capability (`min(4096, model.maxTokens)`) to avoid over-limit provider failures while still preventing truncation. (#11770) Thanks @detecti1. - TUI/Streaming: preserve richer streamed assistant text when final payload drops pre-tool-call text blocks, while keeping non-empty final payload authoritative for plain-text updates. (#15452) Thanks @TsekaLuk. - Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr. +- Daemon/Windows: preserve literal backslashes in `gateway.cmd` command parsing so drive and UNC paths are not corrupted in runtime checks and doctor entrypoint comparisons. (#15642) Thanks @arosstale. - Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane. - Security/Gateway: breaking default-behavior change - canvas IP-based auth fallback now only accepts machine-scoped addresses (RFC1918, link-local, ULA IPv6, CGNAT); public-source IP matches now require bearer token auth. (#14661) Thanks @sumleo. - Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow. diff --git a/src/daemon/schtasks.test.ts b/src/daemon/schtasks.test.ts index 5855951f2b..c2a2fab42f 100644 --- a/src/daemon/schtasks.test.ts +++ b/src/daemon/schtasks.test.ts @@ -245,4 +245,63 @@ describe("readScheduledTaskCommand", () => { await fs.rm(tmpDir, { recursive: true, force: true }); } }); + it("parses command with Windows backslash paths", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-")); + try { + const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd"); + await fs.mkdir(path.dirname(scriptPath), { recursive: true }); + await fs.writeFile( + scriptPath, + [ + "@echo off", + '"C:\\Program Files\\nodejs\\node.exe" C:\\Users\\test\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js gateway --port 18789', + ].join("\r\n"), + "utf8", + ); + + const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" }; + const result = await readScheduledTaskCommand(env); + expect(result).toEqual({ + programArguments: [ + "C:\\Program Files\\nodejs\\node.exe", + "C:\\Users\\test\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js", + "gateway", + "--port", + "18789", + ], + }); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("preserves UNC paths in command arguments", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-")); + try { + const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd"); + await fs.mkdir(path.dirname(scriptPath), { recursive: true }); + await fs.writeFile( + scriptPath, + [ + "@echo off", + '"\\\\fileserver\\OpenClaw Share\\node.exe" "\\\\fileserver\\OpenClaw Share\\dist\\index.js" gateway --port 18789', + ].join("\r\n"), + "utf8", + ); + + const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" }; + const result = await readScheduledTaskCommand(env); + expect(result).toEqual({ + programArguments: [ + "\\\\fileserver\\OpenClaw Share\\node.exe", + "\\\\fileserver\\OpenClaw Share\\dist\\index.js", + "gateway", + "--port", + "18789", + ], + }); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 64729b8953..138fe7c505 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -59,16 +59,14 @@ function parseCommandLine(value: string): string[] { const args: string[] = []; let current = ""; let inQuotes = false; - let escapeNext = false; - for (const char of value) { - if (escapeNext) { - current += char; - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; + for (let i = 0; i < value.length; i++) { + const char = value[i]; + // `buildTaskScript` only escapes quotes (`\"`). + // Keep all other backslashes literal so drive and UNC paths are preserved. + if (char === "\\" && i + 1 < value.length && value[i + 1] === '"') { + current += value[i + 1]; + i++; continue; } if (char === '"') {