Merge pull request #11526 from Kilo-Org/truth-athlete

fix(cli): avoid encoded PowerShell commands
This commit is contained in:
Marius
2026-06-23 10:32:26 +02:00
committed by GitHub
5 changed files with 155 additions and 24 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Run Windows PowerShell tool commands without `-EncodedCommand` to reduce antivirus false positives.
+118 -10
View File
@@ -1,16 +1,124 @@
import { Buffer } from "node:buffer"
export function args(command: string) {
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded(command)]
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script(command)]
}
function encoded(command: string) {
const payload = Buffer.from(command, "utf8").toString("base64")
return Buffer.from(
`[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false);
const setup = `[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false);
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false);
$OutputEncoding = [Console]::OutputEncoding;
& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`,
"utf16le",
).toString("base64")
`
function script(command: string) {
const pos = prologue(command)
const head = command.slice(0, pos)
const body = command.slice(pos)
const gap = head && !/[;\r\n]\s*$/.test(head) ? "\n" : ""
return `${head}${gap}${setup}${body}`
}
function prologue(command: string) {
const pos = scan(command, 0)
const attr = attrs(command, pos)
const body = command.slice(attr)
const match = /^param\s*\(/i.exec(body)
if (!match) return pos
const start = attr + match[0].lastIndexOf("(")
const end = block(command, start, "(", ")")
if (end === undefined) return pos
return end
}
function attrs(command: string, start: number) {
let pos = start
while (pos < command.length) {
const next = scan(command, pos)
if (command[next] !== "[") return next
const end = block(command, next, "[", "]")
if (end === undefined) return start
pos = end
}
return pos
}
function scan(command: string, start: number) {
let pos = start
while (pos < command.length) {
const next = trivia(command, pos)
if (next !== pos) {
pos = next
continue
}
const end = line(command, pos)
const value = command.slice(pos, end)
if (/^using\s+(?:assembly|module|namespace|type)\b/i.test(value)) {
pos = end
continue
}
return pos
}
return pos
}
function trivia(command: string, start: number) {
let pos = start
while (pos < command.length) {
while (/\s/.test(command[pos] ?? "")) pos++
if (command[pos] === "#") {
pos = line(command, pos)
continue
}
if (command.startsWith("<#", pos)) {
const end = command.indexOf("#>", pos + 2)
if (end === -1) return command.length
pos = end + 2
continue
}
return pos
}
return pos
}
function line(command: string, start: number) {
const index = command.indexOf("\n", start)
if (index === -1) return command.length
return index + 1
}
function block(command: string, start: number, open: string, close: string) {
let depth = 0
let quote: string | undefined
for (let pos = start; pos < command.length; pos++) {
const char = command[pos]
if (quote) {
if (quote === "'" && char === "'" && command[pos + 1] === "'") {
pos++
continue
}
if (quote === '"' && char === "`") {
pos++
continue
}
if (char === quote) quote = undefined
continue
}
if (char === "'" || char === '"') {
quote = char
continue
}
if (command.startsWith("<#", pos)) {
const end = command.indexOf("#>", pos + 2)
if (end === -1) return
pos = end + 1
continue
}
if (char === "#") {
pos = line(command, pos) - 1
continue
}
if (char === open) depth++
if (char === close) {
depth--
if (depth === 0) return pos + 1
}
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import * as PowerShell from "@/kilocode/shell/shell" // kilocode_change - encoded PowerShell args
import * as PowerShell from "@/kilocode/shell/shell" // kilocode_change - PowerShell args
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem"
import { which } from "@/util/which"
@@ -189,7 +189,7 @@ export function args(file: string, command: string, cwd: string) {
]
}
if (n === "cmd") return ["/c", command]
if (ps(file)) return PowerShell.args(command) // kilocode_change - encoded PowerShell args
if (ps(file)) return PowerShell.args(command) // kilocode_change - PowerShell args
return ["-c", command]
}
+1 -1
View File
@@ -312,7 +312,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (
function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
if (process.platform === "win32" && Shell.ps(shell)) {
// kilocode_change start - encoded PowerShell args
// kilocode_change start - PowerShell args
return ChildProcess.make(shell, Shell.args(shell, command, cwd), {
// kilocode_change end
cwd,
@@ -1,31 +1,48 @@
import { describe, expect, test } from "bun:test"
import { Buffer } from "node:buffer"
import * as PowerShell from "@/kilocode/shell/shell"
import { Shell } from "@/shell/shell"
const command = `Write-Output "こんにちは 😀"; Write-Output '$value'; Write-Output \`tick\`
Write-Output "done"`
function script(args: string[]) {
return Buffer.from(args[4], "base64").toString("utf16le")
}
describe("PowerShell arguments", () => {
test("transports commands through UTF-8 inside EncodedCommand", () => {
test("transports commands in plaintext with UTF-8 console setup", () => {
const args = PowerShell.args(command)
const value = script(args)
const payload = value.match(/FromBase64String\('([^']+)'\)/)?.[1]
const value = args[4]
expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"])
expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"])
expect(args).toHaveLength(5)
expect(value).toContain("[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)")
expect(value).toContain("[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)")
expect(value).toContain("$OutputEncoding = [Console]::OutputEncoding")
expect(payload).toBeDefined()
expect(Buffer.from(payload!, "base64").toString("utf8")).toBe(command)
expect(value).toContain(command)
expect(args).not.toContain("-EncodedCommand")
expect(value).not.toContain("FromBase64String")
})
test.each(["powershell", "pwsh"])("routes %s through the Kilo argument builder", (shell) => {
expect(Shell.args(shell, command, "/tmp")).toEqual(PowerShell.args(command))
})
test("keeps script-level prologue before the UTF-8 console setup", () => {
const input = `#requires -Version 5.1
using namespace System.Text
<#
Block comment before attributes.
#>
[CmdletBinding()]
[OutputType([string])]
param(
[string]$Name
)
Write-Output $Name`
const value = PowerShell.args(input)[4]
const setup = value.indexOf("[Console]::InputEncoding")
expect(value.startsWith("#requires -Version 5.1")).toBe(true)
expect(value.slice(0, setup)).toContain("[CmdletBinding()]")
expect(value.slice(0, setup)).toContain("[OutputType([string])]")
expect(value.slice(0, setup)).toContain("param(")
expect(setup).toBeLessThan(value.indexOf("Write-Output"))
})
})