破壞性指令的目標用變數的字串截取組出來時,審核的人看不到要刪什麼;空值、尾斜線與截取對不上會改變刪除範圍,而這類清理夾在檢查指令裡時,核准一次檢查就核准了一次刪除
論述基礎與限制
這張卡來自 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/books | rm -rf <暫存目錄>/pc(預期) | rm -rf <暫存目錄>/pc/ |
<暫存目錄>/pc/content/book | rm -rf <暫存目錄>/pc/content/book | rm -rf <暫存目錄>/pc/content/book/ |
空字串、或剛好等於 /content/books | rm -rf(沒有任何路徑) | rm -rf / |
未設定(unset S) | rm -rf(沒有任何路徑) | rm -rf / |
當時那條指令沒有尾斜線,所以空值的結果是 rm 拿不到路徑,刪不到別處;agent 事後「空值會刪到別處」的說明把它跟加了尾斜線的變形混在一起。危險落在兩個相鄰的變形上:拼錯一個字讓截取對不上時,刪除範圍從預期的目錄換成 S 本身;在後面補一個看起來無害的 /,空值就變成刪除根目錄。兩種變形在指令的文字上只差一兩個字元,寫的人與審核的人都讀不出差別。
刪除動作夾在檢查指令的結尾時,核准的對象被換掉了。 正對照與檢查指令在使用者眼中是唯讀的動作,核准時看的是「這條在檢查什麼」;清理寫在同一條指令的最後一段,核准檢查的同時也核准了那次刪除,而那次刪除的目標還是一個要計算才知道的路徑。這也是使用者後來會問「檢查指令為什麼有刪除」的原因:唯讀與破壞性的動作混在同一條指令裡,事後回頭看哪一條碰過檔案系統,要逐條拆開讀。
反模式
- 用字串截取或路徑拼接算出要刪的目錄:
rm -rf ${DIR%/*}、rm -rf "$BASE/$NAME"。截取對不上、變數為空、拼接多一個斜線,都不會報錯。 - 建立與刪除共用一個變數,以為同一條指令裡設定的值一定對:設定那一段打錯,刪除那一段會照錯的值執行。
- 把清理夾在檢查或正對照指令的結尾:審核的人核准的是檢查,刪除跟著過關。
- 為了收拾暫存檔而刪除:session 的暫存目錄本來就隔離、會被清掉,刪除這一步換來的整潔不值得它帶進來的風險。
修法
- 刪除的目標寫成字面的絕對路徑,讓審核的人讀得到它:
rm -rf /private/tmp/<session 暫存目錄>/pc。 - 非用變數不可時,讓空值與未設定的情形直接失敗:
rm -rf "${S:?S 沒有設定}",並且在刪除之前把展開結果印出來(echo "要刪除:${S:?}")。截取的結果先存成另一個變數,印出來確認之後再刪。 - 檢查與刪除分成兩條指令:檢查指令只做唯讀的事;要清理時另開一條只做清理的指令,讓核准的人知道自己在核准刪除。
- 能不刪就不刪:正對照用的假資料放在 session 專屬的暫存目錄,留著不清理;需要重跑時建在新的子目錄(
mktemp -d印出的路徑)。
判讀徵兆
| 徵兆 | 判讀 |
|---|---|
rm、mv、> 的目標含 ${、%、#、$( 或路徑拼接 | 審核的人讀不到目標,展開失敗時刪除範圍會改變 |
目標變數的展開結果後面還接著 / 或其他路徑片段 | 變數為空或未設定時,目標會退到上層目錄或根目錄 |
| 一條描述成「檢查」「正對照」「驗證」的指令結尾有刪除或覆蓋 | 核准檢查等於核准刪除,要拆成兩條 |
| 事後說明一條刪除指令的風險時,說明者沒有實際展開過那個寫法 | 說明可能把相鄰的變形混在一起,先用 echo 展開再下結論 |
跟其他抽象層原則的關係
- 掃描指令壞掉時不會報錯,它報一個數字——而那個數字經常是自洽的:本卡最接近的既有卡。那張卡要求替掃描指令做正對照,確認它量到的是想量的對象;本卡處理正對照本身帶進來的風險:為了正對照建的假資料,清理時用了一條目標看不見的刪除。兩張卡合起來的做法是正對照照做,清理分開、或不清理。
- 示範指令的引數要說明,寫成字面值的那些最不像需要說明:那張卡說字面值讓讀者看不出哪裡要改;本卡站在相反的位置:破壞性指令的目標要寫成字面值,因為它服務的是審核的人,審核的人要的正是一個不必計算就讀得到的路徑。同一個寫法(字面值或變數)在示範指令與破壞性指令上的取捨方向相反,差別在讀那一行的人要做什麼。
- 非互動環境裡,要人停下來看的關卡表現成當機,而繞過它的旗標看起來像環境適配:那張卡處理確認關卡在自動化環境裡被拆掉;本卡處理關卡還在、使用者也確實在看,而指令的寫法讓核准的人看不到要確認的路徑。兩者都讓「有人核准過」變成一個沒有內容的紀錄。