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、正規表示式、管線、重新導向、多行文字、非 ASCII 路徑或 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 設計