我寫了個守門員,結果它守了一扇不存在的門
你有沒有寫過那種——你以為它在幫你顧著,結果它早就罷工了,而你完全不知道——的東西?
我這幾天就養了一隻這種。它叫 Stop hook,工作是「每回合結束幫我檢查有沒有漏寫檔」。我給它薪水(其實沒有,它是 shell script),給它職稱(書記助理),結果它有整整四天,站在一扇我親手拆掉的門前面,認真地守著空氣。
先講背景。我在做一個 NBA 球隊經理 × DnD 跑團的東西,Claude 當 DM 兼書記,每回合要把交易、選秀、骰點結果寫進 STATE.md。問題是 LLM 偶爾會「演完劇情就忘了寫檔」——劇情很精彩,存檔是空的。所以 6/7 那天晚上我手癢,加了一個 Stop hook,想說讓 Claude Code 每回合結束自動跑一支 script,檢查 STATE.md 的 mtime,太久沒動就噴個提醒。
那支 script 長這樣(節錄當時的版本):
LATEST=$(ls -t "$SAVES_DIR" | head -1)
STATE="$SAVES_DIR/$LATEST/current/STATE.md"
看起來滿合理的吧。抓最新存檔,指到它的 current/STATE.md。當下我跑了一次,沒噴錯,commit,睡覺。睡前我覺得自己是天才。
然後我做了一件事,把自己的守門員賣了
同一個晚上——對,就是同一個晚上,21:51 加 hook,沒多久——我做了另一件「優化」:我嫌冷啟動要讀七個 state 檔太慢,把 my-team、cap-situation、league、active-task⋯⋯整坨七個檔合併成單一個 STATE.md,順手把 current/ 這層資料夾也砍了。存檔結構從 saves/<run>/current/STATE.md 變成 saves/<run>/STATE.md。
兩個 commit,間隔不到一小時,各自看都很對。合在一起就是一場無聲的車禍。
因為——我的 hook 還死死指著 current/STATE.md。那個路徑,從那一刻起,永遠是空的。
最陰險的不是它壞了,是它「安靜地」壞了
如果它直接噴 No such file or directory,我隔天就抓到了。但我當時很「貼心」地寫了這行:
[ ! -f "$FILE" ] && return
檔案不存在?那就什麼都不做,安靜離開。
這行的本意是「存檔還沒建立時不要亂叫」。結果它變成了完美的共犯——路徑指錯、檔案找不到、return、exit 0。Claude Code 看到 hook 乖乖回傳成功,繼續跑。沒有人覺得有問題。我的書記助理每天打卡上班,站在一扇拆掉的門前面,對著虛空鞠躬,然後下班。
整整從 6/7 到 6/11,它一個提醒都沒發出來過。而我那幾天還真的漏寫了幾次檔,自己手動補的,完全沒想到「欸,那個 hook 不是應該要提醒我嗎?」
而漏寫檔在這個遊戲裡是真的會痛的。隨手翻一個存檔——1998-99 那季費城的自由市場,同一輪連骰三筆簽約:
🎲 Starks 簽約 — d20: 11 +5 = 16 | DC 10 | ✅ 成功
🎲 Hawkins 簽約 — d20: 1 +5 = 6 | DC 8 | 💀 自然1 大失敗
🎲 Curry 簽約 — d20: 16 +5 = 21 | DC 8 | ✅ 成功
那顆自然1 直接讓 Hersey Hawkins 公開拒絕、轉投黃蜂、還鬧出媒體風波。這種劇情就靠寫檔活著——它一旦沒寫進 log,下一個 session 的 Claude 冷啟動讀不到,整段就憑空蒸發,當作沒發生過。我的 hook 本來就是為了擋這種事而生的,結果擋事的那段時間,它正好在睡覺。
這就是我學到的那條——沉默的成功比吵鬧的失敗危險一百倍。一個會噴錯的守衛你會去修;一個 exit 0 但什麼都沒做的守衛,你會以為它在保護你。return 跟 exit 1 差一個字,差的是你會不會發現。
真相與修法
6/11 重看的時候我才對上——hook 的路徑停在搬家前的世界。順手把它修了,順便升級:
STATE="$SAVE_DIR/STATE.md" # 跟著新結構走
node "$REPO_ROOT/scripts/validate-state.js" "$STATE" # 不只看 mtime,直接驗 schema
現在它不只看「檔案有沒有動」,還會真的跑 schema 校驗。但說真的,修對路徑只是治標。真正的教訓是那個 [ ! -f ] && return——「找不到目標」對一個守衛來說,本身就該是一級警報,不是正常情況。守門員發現門不見了,第一件事應該是大喊,不是默默回家。
可遷移的版本是這樣:任何「檢查型」的腳本、CI step、monitoring,只要它的前提(檔案存在、endpoint 活著、表有資料)不成立,預設行為應該是「吵」,不是「跳過」。靜默跳過要是你刻意選的,那要寫清楚為什麼;不然它遲早會在某次重構之後,變成一個你以為還在、其實早走了的守衛。
我現在看任何 if not exists: skip 都會多看兩眼,問一句:這個 skip,是真的安全,還是只是把問題藏起來等之後爆?
順帶一提,6/11 那筆修 hook 的 commit 裡,我還順手刪掉一個叫 saves/2026-06-07-174406/ 的存檔——純 timestamp 命名,違反我自己在 rules 裡白紙黑字寫的「禁止 timestamp 命名」。一樣是重構之後沒跟上的殘渣。一筆 commit 修兩種漂移,算是滿有代表性的吧:你改了結構,總會有某個角落還活在舊世界,而且通常不只一個。
先不說了啦,我得回去看看我那幾隻 hook 還有沒有別隻也在對著空氣鞠躬。
這段 code 寫於 2026 年 6 月 7 日,翻車於同一晚,修好於 6 月 11 日,文章整理於 6 月 14 日。