跳到主要內容
maxstudioNews
AI 自動化DEV Community (Claude Code)約 6 分鐘

Claude Code 分支保護鉤子錯誤:一個空白字元引發的教訓

作者在為 Claude Code 撰寫分支保護鉤子時,因一個額外的空白字元而發現了程式錯誤。這個錯誤導致錯誤訊息回報了錯誤的分支名稱,凸顯了為 AI 代理設計精確錯誤回饋的重要性,以避免誤導其後續推理。

Claude Code 分支保護鉤子錯誤:一個空白字元引發的教訓
圖片來源:DEV Community (Claude Code)

上週,我發布了一套四個 Claude Code 鉤子,用於捕捉常見的 AI 代理錯誤,包括無效編輯、推送到錯誤分支、超出範圍的寫入以及回合結束時未經驗證的聲明。我在第一篇文章中介紹了它們,並在第二篇文章中討論了測試。這篇文章是一個小而尷尬的後續:一個我在對鉤子本身進行「狗糧測試」(dog-fooding)時發現的錯誤。

block-wrong-branch.sh 鉤子在 PreToolUse(Bash) 階段針對 git push 命令執行。它會提取目標分支並與 .claude/branch-policy.json 進行比對。簡化後的程式碼如下:

```bash

Only inspect git push commands.

case "$command" in

"git push" | "git push " ) : ;;

  • ) exit 0 ;;

esac

Tokens after "git push".

tail="${command#*git push}"

read -r -a tokens <<< "$tail"

...pick the branch out of the positional args...

```

偵測步驟接受 git push 和 git push (我知道實際命令中會出現雙重空白字元,因為人們會複製貼上)。解析步驟使用 ${command#*git push} 來移除 git push 及其之前的所有內容。你看到錯誤了嗎?我一個星期都沒發現。

我在撰寫新的測試案例時,不小心輸入了一個帶有雙重空白字元的命令:{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}。將其輸入鉤子後,結果是 exit 2,標準錯誤輸出顯示:「Blocked: git push to branch "push" is not allowed by .claude/branch-policy.json. Reason: not in allow-list [...]」。

阻擋是正確的,因為沒有任何內容符合允許列表,所以故障安全機制啟動。但它回報的分支名稱是「push」,而不是「main」。這是因為 ${command#*git push} 模式匹配的是字面上的 git push(一個空白字元)。輸入命令中有兩個空白字元。

Shell 參數擴展不會壓縮空白字元,因此前綴模式不匹配。tail 變數保持不變,仍然是完整的命令 git push origin main。然後 read -r -a tokens <<< "$tail" 將其標記化。Bash 的單詞分割會壓縮空白字元,因此 tokens 變成了 [git, push, origin, main]。解析器將第二個位置參數選為分支,結果就是「push」。

案例語句的萬用字元匹配對空白字元敏感,參數擴展也對空白字元敏感,但單詞分割卻對空白字元不敏感。這二十行 Bash 程式碼中包含了三種不同的規則。偵測步驟容忍了雙重空白字元,但解析器卻沒有。

解決方案很簡單,在命令處理的開頭加上一行:command="$(printf '%s' "$command" | tr -s ' \t')"。這行程式碼會正規化空白字元,讓 git push 和 git push 都能被正確解析。

我還增加了一個回歸測試("git push origin main" 必須回報被阻擋的分支是 main,而不是 push)。測試數量從 26 個增加到 27 個,並作為 v1.3 版本發布。

我寫這篇文章有兩個原因。第一,鉤子的阻擋是正確的,它沒有讓錯誤的推送通過。但錯誤訊息卻謊報了原因。這正是我建立此工具來防止 AI 代理出現的行為:不是災難性的錯誤操作,而是關於發生了什麼的小而自信、形狀錯誤的聲明。「Blocked branch push」是一個事實錯誤的回饋字串,LLM 會試圖對此進行推理(「嗯,我沒有嘗試推送一個名為 push 的分支,是嗎?」)。「狗糧測試」在任何用戶發現之前就捕捉到了這個錯誤。

第二,當你為 LLM 建立鉤子腳本時,LLM 會讀取你的標準錯誤輸出並據此進行推理。模糊或錯誤的錯誤字串不僅會傷害用戶,還會毒害代理的下一個回合。「Blocked: branch push not allowed」在某些方面比沒有回饋更糟,因為現在代理將嘗試繞過一個不存在的限制。

值得在自己的鉤子解析器上測試的對抗性輸入包括:命令中的雙重或尾隨空白字元、包含 /、. 或 -force 作為字串(而非旗標)的分支名稱、帶有 src:dst 的引用規範、空標準輸入或格式錯誤的 JSON,以及缺少 tool_input 鍵的正確 JSON。我的 27 個測試套件在 20 毫秒內運行完成。如果你的鉤子沒有測試套件,它們就會有這個錯誤。

verify-write.sh:在 PostToolUse(Edit|Write|MultiEdit) 階段執行,將檔案與索引進行差異比對。如果變更了零位元組,則以修正提示退出並顯示 exit 2。捕捉 LLM 聲稱「我更新了 X」的無效編輯。

block-wrong-branch.sh:在 PreToolUse(Bash) 階段針對 git push 執行,從命令中解析分支並與 .claude/branch-policy.json 進行比對。故障安全:未知分支,阻擋。

scope-fence.sh:在 PreToolUse(Edit|Write) 階段執行,將目標路徑與 .claude/scope.json 中的允許/拒絕萬用字元進行比對。故障安全。

audit-turn.sh:在 Stop 階段執行,掃描最終的助理訊息,尋找「我更新/建立/添加了 X」風格的聲明,並與 trust-audit.log 進行交叉參考。如果檔案不在日誌中,則以「要麼進行更改,要麼修正你之前的訊息」阻擋。它有一個 stop_hook_active 循環保護,這是第二篇文章的讀者指出的缺失部分,已在 v1.2 中修復。

這套工具包含鉤子、安裝腳本、27 個測試套件以及兩個策略 JSON 檔案。採用 MIT 授權。可在 Gumroad 上購買,並提供啟動優惠券(SHOWDEV50 可享半價)。如果你建立鉤子,並且你的解析器在某些奇怪的輸入上出錯,請回覆並提供負載,我會用我的工具進行測試。正是這種回饋讓 v1.1、v1.2 和 v1.3 版本一個比一個更好。這篇文章是在 AI 協助下撰寫並經準確性審查。

Share

覺得值得一看?分享給朋友。

以上為 AI 自動翻譯導讀。原文版權歸 DEV Community (Claude Code) 所有, 建議透過「閱讀原文」前往原始網站,以取得最完整資訊並支持原作者。

Newsletter

不用每天追,AI 重點每週寄給你

43 個來源、每 2 小時更新,太多看不完。電子報每週挑最值得看的幾則,附上 Max 的看法。

每週一封。Email 只用來寄電子報,每封信都能一鍵退訂。