論述基礎與限制

這張卡來自 2026-10-29 一個 Claude Code session(macOS、zsh 5.9)。agent 替站內的連結檢查工具做正對照:在 session 的暫存目錄底下建一個假的內容目錄,放一個壞連結的檔案,跑檢查,最後刪掉那個目錄。整段寫成一條指令,刪除的目標用參數展開的字串截取組出來:

1# 當時的寫法:目標是 S 去掉結尾的 /content/books 之後剩下的路徑
2S=<暫存目錄>/pc/content/books; mkdir -p $S && <建檔與檢查>; rm -rf ${S%/content/books}

這一條執行完,刪掉的是暫存目錄底下 agent 自己建的 pc/,沒有刪到其他檔案。之後使用者中斷了另一條檢查指令,問檢查指令為什麼有刪除的動作;那一條沒有 rm,而這次對話讓前面那條正對照指令被重新檢視。agent 當下的說明是「S 是空值時,刪除的對象會落到別處」,之後用 echo 實測展開結果,發現這句說法不精確,實測結果寫在核心原則裡。

個案只有這一次,而且沒有造成刪錯;下面的展開結果是在 zsh 5.9 用 echo 量的,沒有實際執行任何刪除,bash 的展開規則在這幾個寫法上相同,這一點沒有另外實測。

核心原則

破壞性指令的目標用變數組出來時,指令的文字不包含要刪的路徑。 審核這條指令的人(核准工具呼叫的使用者、讀 log 的人、事後查 transcript 的人)看到的是 rm -rf ${S%/content/books},要自己在腦中算出 S 的值、再套一次截取規則,才知道刪的是哪個目錄。審核的實際動作是讀指令的文字,所以目標不在文字裡時,審核等於沒有發生。

字串截取的失效不報錯,它換一個刪除範圍。 ${S%pattern} 去掉 S 結尾最短一段符合 pattern 的字串,對不上的時候原樣傳回 S。在 zsh 5.9 用 echo 量到的展開結果:

S 的值rm -rf ${S%/content/books} 展開成加尾斜線 rm -rf ${S%/content/books}/ 展開成
<暫存目錄>/pc/content/booksrm -rf <暫存目錄>/pc(預期)rm -rf <暫存目錄>/pc/
<暫存目錄>/pc/content/bookrm -rf <暫存目錄>/pc/content/bookrm -rf <暫存目錄>/pc/content/book/
空字串、或剛好等於 /content/booksrm -rf(沒有任何路徑)rm -rf /
未設定(unset S)rm -rf(沒有任何路徑)rm -rf /

當時那條指令沒有尾斜線,所以空值的結果是 rm 拿不到路徑,刪不到別處;agent 事後「空值會刪到別處」的說明把它跟加了尾斜線的變形混在一起。危險落在兩個相鄰的變形上:拼錯一個字讓截取對不上時,刪除範圍從預期的目錄換成 S 本身;在後面補一個看起來無害的 /,空值就變成刪除根目錄。兩種變形在指令的文字上只差一兩個字元,寫的人與審核的人都讀不出差別。

刪除動作夾在檢查指令的結尾時,核准的對象被換掉了。 正對照與檢查指令在使用者眼中是唯讀的動作,核准時看的是「這條在檢查什麼」;清理寫在同一條指令的最後一段,核准檢查的同時也核准了那次刪除,而那次刪除的目標還是一個要計算才知道的路徑。這也是使用者後來會問「檢查指令為什麼有刪除」的原因:唯讀與破壞性的動作混在同一條指令裡,事後回頭看哪一條碰過檔案系統,要逐條拆開讀。

反模式

  • 用字串截取或路徑拼接算出要刪的目錄:rm -rf ${DIR%/*}、rm -rf "$BASE/$NAME"。截取對不上、變數為空、拼接多一個斜線,都不會報錯。
  • 建立與刪除共用一個變數,以為同一條指令裡設定的值一定對:設定那一段打錯,刪除那一段會照錯的值執行。
  • 把清理夾在檢查或正對照指令的結尾:審核的人核准的是檢查,刪除跟著過關。
  • 為了收拾暫存檔而刪除:session 的暫存目錄本來就隔離、會被清掉,刪除這一步換來的整潔不值得它帶進來的風險。

修法

  1. 刪除的目標寫成字面的絕對路徑,讓審核的人讀得到它:rm -rf /private/tmp/<session 暫存目錄>/pc。
  2. 非用變數不可時,讓空值與未設定的情形直接失敗:rm -rf "${S:?S 沒有設定}",並且在刪除之前把展開結果印出來(echo "要刪除:${S:?}")。截取的結果先存成另一個變數,印出來確認之後再刪。
  3. 檢查與刪除分成兩條指令:檢查指令只做唯讀的事;要清理時另開一條只做清理的指令,讓核准的人知道自己在核准刪除。
  4. 能不刪就不刪:正對照用的假資料放在 session 專屬的暫存目錄,留著不清理;需要重跑時建在新的子目錄(mktemp -d 印出的路徑)。

判讀徵兆

徵兆判讀
rm、mv、> 的目標含 ${、%、#、$( 或路徑拼接審核的人讀不到目標,展開失敗時刪除範圍會改變
目標變數的展開結果後面還接著 / 或其他路徑片段變數為空或未設定時,目標會退到上層目錄或根目錄
一條描述成「檢查」「正對照」「驗證」的指令結尾有刪除或覆蓋核准檢查等於核准刪除,要拆成兩條
事後說明一條刪除指令的風險時,說明者沒有實際展開過那個寫法說明可能把相鄰的變形混在一起,先用 echo 展開再下結論

跟其他抽象層原則的關係