Codex 大战 PowerShell:powershell-safe-invocation Skill 解决了什么

从一次 Codex 在 PowerShell 里反复修脚本的截图说起,整理 Windows 下让 Agent 安全调用 PowerShell、写文件和传参的实用规则。

最近看到一张很有节目效果的截图:Codex 在 PowerShell 里修一个 Markdown 文件,先是 here-string 没执行,接着 CRLF/LF 匹配失败,再到反引号被 PowerShell 解析、路径反斜杠处理不稳、-split 参数写法踩坑。模型一路自我诊断,一路继续补脚本,最后评论区给它起了个很准确的标题:经典 GPT 5.5 大战 PowerShell。

Codex 在 PowerShell 中反复修脚本的截图

截图里这串连续补救很有梗,但它背后的坑并不罕见。

这图好笑,是因为它太像真实开发现场。PowerShell 并不是简单的“Windows 版 Bash”。它有自己的字符串规则、参数绑定规则、cmdlet 错误模型和原生命令传参方式。Agent 一旦把 Bash 习惯带进 PowerShell,写文件、替换文本、调用外部程序这些看似普通的任务,就会变成连续小翻车。

Misaka-Mikoto-Tech 的 powershell-safe-invocation skill,正好把这些坑收束成了一套简洁规则。它不是教人“多背 PowerShell 语法”,而是告诉 Agent:在 Windows 上执行命令时,先选不容易出错的调用形态。

最大的问题不是 PowerShell 难,而是 Agent 容易把它当 Bash

截图里的几个问题很典型。

第一类是字符串和换行。Markdown 里有反引号,PowerShell 也把反引号当转义符;文档可能是 CRLF,脚本却按 LF 精确匹配;here-string 的起止格式只要缩进或引号不对,就可能根本没有按预期生成文本。

第二类是路径和参数。Windows 路径天然带反斜杠,目录里还常有空格、中括号和中文。把命令拼成一整串再执行,很容易让某个参数被拆开,或者让某段路径被当成转义、通配符、正则或另一个 shell 的语法。

第三类是错误判断。外部程序要看 $LASTEXITCODE,PowerShell cmdlet 则应该依赖终止错误和 $ErrorActionPreference = 'Stop'。如果 Agent 混用这两套模型,就会出现“命令其实没成功,但脚本以为成功了”的状态。

这些都不是大故障,却很消耗注意力。Agent 还会因为“看起来已经执行过命令”而继续在错误假设上修补,越修越绕。

这个 skill 的核心:不要拼大字符串

powershell-safe-invocation 里最重要的一条,是不要把可执行文件、路径、参数和引号拼成一个巨大的命令字符串。

调用原生程序时,应该把每个参数作为数组里的一个独立元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$exe = 'C:\Path With Spaces\tool.exe'
$args = @(
    '--input'
    'C:\Data Folder\input.json'
    '--flag'
)

& $exe @args

$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
    throw "$exe failed with exit code $exitCode"
}

这段看起来比一行命令啰嗦,但它把风险拆开了:路径就是路径,参数就是参数,调用就是调用。Agent 不需要猜这一层引号是给 PowerShell 的、给 cmd 的,还是给目标程序的。

对 PowerShell cmdlet,则更适合用 hashtable splatting:

1
2
3
4
5
6
7
8
$params = @{
    LiteralPath = 'C:\Data[1]\input.txt'
    Destination = 'C:\Output'
    Force       = $true
    ErrorAction = 'Stop'
}

Copy-Item @params

这里的关键是 -LiteralPath。真实路径默认按字面量处理,只有确实需要通配符时才使用会展开的路径参数。对 Agent 来说,这比临时判断路径里有没有 []*? 更稳。

复杂命令别塞进 -Command

很多 Windows 自动化事故,都从这一类命令开始:

1
cmd.exe /c pwsh.exe -Command "..."

里面一旦出现 JSON、XML、正则、管道、重定向、多行文本、中文路径或 Markdown 反引号,转义层数就会迅速失控。对人来说难读,对 Agent 来说更容易误判。

skill 给出的选择很朴素:复杂一点就写临时 .ps1 文件,再用 pwsh.exe -NoLogo -NoProfile -NonInteractive -File script.ps1 执行。

这条规则特别适合 Agent。因为 Agent 生成脚本文件后,可以分步骤检查脚本内容、运行结果和文件差异。把所有东西塞进一条 -Command,失败时只剩一团很难定位的引号和转义。

powershell.exe 不是 pwsh.exe

另一个容易被忽略的点是版本。

安装 PowerShell 7,并不会让 powershell.exe 自动变成 PowerShell 7。通常:

  • powershell.exe 是 Windows PowerShell 5.1
  • pwsh.exe 是 PowerShell 7

两者在原生命令参数传递、编码默认值和一些行为细节上并不完全相同。Agent 如果不知道自己跑在哪个 shell 里,就应该先检查:

1
2
$PSVersionTable.PSVersion
$PSNativeCommandArgumentPassing

这不是仪式感。很多“我本地可以,Agent 跑不行”的问题,根源就是 shell 版本和参数传递规则不同。

写文件时,编码和换行要当成一等问题

截图里最有意思的地方,是 Agent 反复围绕 Markdown 文件替换失败打转。这里面至少有三个隐含前提:

  • 文件实际编码是什么
  • 文件换行是 CRLF 还是 LF
  • 替换目标是否被 PowerShell 字符串语法改变过

中文文章、Markdown 代码块、front matter、短代码、反引号混在一起时,最好不要用随手拼出来的 PowerShell here-string 写入正文。要么使用明确编码的文件 API,要么让脚本本身只处理 ASCII 控制逻辑,把非 ASCII 正文交给更可靠的写入路径。

对 Agent 工作流来说,还有一条额外经验:写完文件后立刻验证。至少检查 UTF-8 可读、front matter 闭合、Markdown 链接没有断、正文里没有连续问号或明显 mojibake。很多乱码问题越早发现越便宜。

一套更稳的决策顺序

这个 skill 最实用的部分,是给 Agent 一个决策顺序:

  1. 能用 PowerShell cmdlet,就用 cmdlet。
  2. 调用外部程序时,用 & $exe @args
  3. 命令复杂时,写 .ps1 文件并用 pwsh.exe -File
  4. 必须精确控制进程参数时,用 ProcessStartInfo.ArgumentList
  5. 只有需要新窗口、提权、脱离当前进程等特殊行为时,才用 Start-Process
  6. 只有确实需要 cmd 语义时,才套 cmd.exe /c
  7. Invoke-Expression 放到最后,且要非常克制。

这套顺序的好处是,它把“能跑”变成“可解释、可检查、可复现”。对人类开发者如此,对 Agent 更是如此。

给 Codex 的现实建议

如果让 Codex 在 Windows 仓库里改文件,我会给它几条硬约束:

  • 不要用 Bash 风格的 \" 去写 PowerShell。
  • 不要把路径和参数拼成一整条字符串。
  • 不要用 Start-Process -ArgumentList 处理复杂参数。
  • 不要用 cmd.exe /c 包一层来解决普通调用问题。
  • 对真实文件路径优先使用 -LiteralPath
  • 对复杂脚本优先写 .ps1,再用 pwsh.exe -File 跑。
  • 写入中文、日文等非 ASCII 内容后,必须检查编码和乱码标记。

PowerShell 并不是 Agent 的敌人。真正的问题是 Agent 太容易沿用“拼字符串,然后交给 shell 猜”的旧习惯。只要把调用边界拆清楚,PowerShell 反而能提供很强的结构化能力。

所以这场“Codex 大战 PowerShell”的结论不是让 Agent 避开 Windows,而是给它一份战术手册:少赌引号,多用结构;少塞一行命令,多保留可检查的中间状态。

笑点在截图里,经验在规则里。

记录并分享
使用 Hugo 构建
主题 StackJimmy 设计