09-07 三種寫入在雲端全被 auto mode 分類器擋:npx wrangler secret put、 stage-deploy-artifacts.py --confirm、curl POST 到 arcrun-yuga3bse—— 白名單是逐條比前綴,複合指令與任意 curl 永遠對不上;放整類又是紅線。 - scripts/stage secret put|list|delete:直接打 CF API(不經 npx);先 GET /accounts 看這把 token 打得到誰,stage 的 ID 不在裡面就拒絕並印出它打得到誰;URL 永遠只帶 1129efd7…;值只從 --from-env 讀,--value 拒收。 - scripts/stage api <METHOD> <worker>/<path>:curl 打實例,主機只會是 *.arcrun-yuga3bse.workers.dev,別的主機拒絕;標頭與 body 走 stdin 設定檔+管線, 不走 argv;--bearer-env/--header-env/--data-env 從環境變數讀。 - docs/permissions-allow.json:stage 四種形狀+Arcrun 自己那支 stage-deploy-artifacts.py 的相對形狀(它已寫死只認 youlin 帳號 ID、不收 --account/--token,不重造)。 - scripts/test-stage.sh 45 條(假 CF 對 PUT 回 201——第一次實跑 CF 回 201 被講成失敗, 就是這格抓的);test-settings-allow-sync 仍 19/19。 - 真跑:探針種進 youlin arcrun-cypher-executor、CF API 獨立列到、刪掉恢復原狀; api GET health 200、無 Bearer POST 401、leo21c 主機拒絕零請求。 「帶 Bearer 真寫 KBDB」缺 KBDB_INTERNAL_TOKEN 沒跑(TESTING A36 標明)。 盤點在本樹實數:hooks 61、註冊 85(都沒動)、scripts 56(+stage、+test-stage.sh)。 版本待總管定版。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTZ9QtvjY7MNxjfQbexAm7
22 KiB
已知誤解 / 踩過的坑
這是 ISEP 自己的坑,不是 InkStoneCo 的(不轉抄,複製即 fork,fork 即漂移)。 撞到新坑就 append 一條;session 開場只推最近幾條標題(見
hooks/session-start-recall.shpush 5/5),全文在這裡。
⚠️ MISTAKE: 「環境設定」曾經拆成兩份,各自會漂
症狀: 本機在跑 InkStoneCo/.claude/(真身),雲端跑的是 generate-shell-payload.py
另外產生塞進 GitHub 私 repo 的一份(薄殼)。inkstone/InkStoneCo#57 實測:薄殼比真身
少 7 支閘,其中兩支是前一天才立的;#14 更早查到雲端 33 支 guard 一支都沒生效。
正確做法: 只留一份——ISEP 這個 repo 本身就是唯一真相源,本機與雲端裝同一個 plugin。
改動一律只改這裡,然後兩邊各自 /plugin update。不要再改
InkStoneCo/.claude/hooks/(退場中,早晚會刪)。
原因: 「一份東西兩個副本」在沒有機制強制同步的情況下必然漂移——差異不是誰疏忽,
是結構本身允許漂移發生。
日期: 2026-08-20(ISEP c263866 建立時就是為了解這個病)
⚠️ MISTAKE: hook 路徑寫死會在雲端斷
症狀: 舊版 hook 若用 $CLAUDE_PROJECT_DIR/.claude/hooks/... 或寫死的絕對路徑指向 hook
腳本自己,雲端執行時的 cwd 不是本機那個真身目錄,路徑就對不到、hook 直接失效
(正是上一條「雲端 33 支 guard 一支都沒生效」的根因)。
正確做法: hook 指自己(找到自己在哪、要 source 的其他 hook 檔)一律用官方
${CLAUDE_PLUGIN_ROOT}。腳本內部要指專案裡的檔案(如 system-dev/wiki/、
system-dev/docs/)才用 $CLAUDE_PROJECT_DIR——那些檔案本來就该住在被操作的
那個 repo 裡,跟 hook 自己的路徑是两回事,别混。
原因: 兩種路徑指的是完全不同的東西(「plugin 安裝到哪」vs「正在操作哪個專案」),
混用就是這條坑的直接原因。
日期: 2026-08-20(README「路徑規約」段記錄,ISEP 0.1.0 把 51 條 hook 路徑全部改過一輪)
⚠️ MISTAKE: 「裝好了」不等於「它在跑」
2026-08-20 實查:ISEP repo 建好、README 寫著 0.1.0、41 支閘都在裡面——
但 claude plugin list 裡根本沒有 ISEP。本機仍然在跑 InkStoneCo/.claude/,
Gitea 上一個 release tag 都沒有。
⇒ 「東西做出來了」與「有人在用它」是兩件事,而只有後者算交付。
⇒ 判準:去執行環境查它有沒有被載入(claude plugin list / claude plugin details),
不要從 repo 裡有什麼檔案去推論。
⚠️ MISTAKE: 判準寫在閘裡了,但那個閘掛在做完之後才跑的時機上
票: inkstone/InkStoneCo#55
日期: 2026-08-26
症狀: leo 一天內好幾次被丟純技術路徑選擇,當場問「今天已經好幾次問我, 為什麼 hooks 沒有攔下來?」,其中一次他直接說「這種問題不要問我, 我要的是你解決了以後給我 prod」。
實查: 總管問 leo 走的動作是 AskUserQuestion 這個工具,而
hooks.json 裡 AskUserQuestion 出現 0 次——沒有任何 matcher,它是裸的。
判準其實早就寫好了(self-drive-police.sh / self-drive-judge.sh 用的就是四題公式),
但那兩支只掛在 Stop 與 SubagentStop。
原因: 判準對了,時機錯了。 Stop 是回合結束後才跑——問題早就送到 leo 眼前、
他早就被打斷了,這時再反問 AI「你查過了嗎」,成本已經轉嫁出去了。
正確做法: 攔截點要長在那個動作發生的那一刻(PreToolUse / AskUserQuestion)。
新增 hooks/ask-user-question-guard.sh。
🔴 推廣:以後看到「規則寫了卻沒被攔下來」,先問的不是「判準對不對」,
而是「這支閘掛在哪個事件上、那個事件發生時傷害造成了沒有」。
⚠️ MISTAKE: hook 訊息用沒加引號的 heredoc,反引號會被當成命令執行
票: inkstone/InkStoneCo#55
日期: 2026-08-26
症狀: ask-user-question-guard.sh 擋下之後,stderr 冒出
line 218: system-dev/wiki/: is a directory,而訊息裡
「去查 system-dev/wiki/」和「touch /tmp/.ask-ok-<session_id>」兩行
變成空白。閘照擋 exit 2,所以測試若只看離開碼完全看不出來。
原因: 寫成 cat >&2 <<EOF(heredoc 標記沒加引號)⇒ shell 會對內容做展開,
而本 repo 的 hook 訊息慣例上大量使用反引號標路徑與指令
⇒ 每一組反引號都被當成命令替換真的去執行。
正確做法: hook 的訊息一律用 cat <<'EOF'(標記加單引號)。
需要塞變數就留 __PLACEHOLDER__,事後用 python 換掉——
不要用 sed,正體中文加上訊息裡的 /、&、\ 讓跳脫非常脆。
迴歸測試要檢查訊息內容,不能只檢查離開碼
(hooks/tests/ask-user-question-guard.test.sh 的 ⑩b 就是這一條)。
⚠️ MISTAKE: 閘只驗了規則的殼,沒驗規則本身
no-ticket-no-dispatch.sh 掛在派工的當下,檢查「派工單裡有沒有一行 【工單】owner/repo#N」。
規則的原文卻是「不准把票上已經有的東西再抄一遍進派工單⋯⋯派工單只寫票號」。
⇒ 於是可以把 40 行任務全寫在 prompt 裡、票號補一行,閘照樣放行。 ⇒ 2026-08-27 一天之內這樣做了 5 次,每一次票上都沒有那份任務。 leo:「這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?」
根因不是那支閘寫壞了,是它驗的東西比規則小。 「有沒有票號」是規則最容易機械化的那一格,所以它被實作了; 「任務有沒有真的落在票上」比較難,所以沒有——而漏掉的那格才是規則的本體。
⇒ 判準:寫完一支閘,回頭把規則原文逐句對一次,問「這一句被驗到了嗎」。
只驗得到最容易的那一格 ⇒ 那支閘會製造「有在管」的錯覺,比沒有閘更危險。
⇒ 同款:history-first/KBDB-first/stage-first/AskUserQuestion 裸奔,全是這個形狀。
修法(v0.5.0):dispatch-format-guard.sh——派工單 = 票號,多一個字都擋。
規則變得比原本更嚴,反而更好驗:判準從「內容夠不夠」變成「這一行是不是【工單】欄位」,
純結構、不用語意判官、每次結果一樣。
規約:docs/governance/dispatch-and-reply-format.md。
日期: 2026-08-27(inkstone/ISEP#30 comment 4322/4325/4327)
⚠️ MISTAKE: 「這是 session 才知道的事」被當成寫進 prompt 的正當理由
派工鐵律允許派工單帶「這個 session 才知道、票上還沒有的事」。
總管照字面理解,把 TCC 權限、main 是哪顆 commit、正本能從哪裡 clone 三件事寫進 prompt。
leo 當場:「這些為什麼不寫到票裡?」
⇒ 「票上還沒有」不是把它寫進 prompt 的理由——它就是「去把它寫上票」的指令。 ⇒ 實害(同日):總管停掉重派 3 次,前兩次的任務與 session 事實全部隨 prompt 蒸發。 票活得比任何一個 agent 久,prompt 不是。
判準:「這句話換一張票還成立嗎?」
還成立 ⇒ 共通規定(docs/governance/dispatch-and-reply-format.md §2,機器自動注入)。
只有這次成立 ⇒ 寫進那張票。兩種都不進派工單。
日期: 2026-08-27(inkstone/ISEP#30 comment 4327)
⚠️ MISTAKE: fail-closed 的閘,會把「執行裁決」這個動作本身變成不可執行
票: inkstone/ISEP#91(裁決原文在 inkstone/InkStoneCo#40 → comment 2942)
日期: 2026-08-31
症狀: leo 2026-08-16 裁定「取消 Active SDD」——任務狀態搬到 Gitea 管, SDD 只記「起初的樣子」,文件不帶任務 checkbox。 11 天過去,這個裁決一個字都沒被執行。
實查: sdd-guard.sh 是 fail-closed 的:動 code 檔時 status: active 的 SDD
不是恰好 1 份就擋(0 份也擋),路徑所在的 repo 沒有 3-specs 也擋。
⇒ 照裁決把最後那份 active 拿掉 → 變 0 份 → 任何人動任何
.ts/.py/.go 全部被擋。當時沒出事,純粹是因為剛好還剩 1 份。
而 ISEP 這個 repo 自己根本沒有 3-specs——本票開工第一件事就實測到:
改自己的 hooks/lib/*.py 當場被這支閘擋下(exit 2)。它一直在誤攔,只是沒人回報。
原因: 不是有人偷懶,是沒有人把裁決和那支閘連起來看。 裁決被讀到了、也沒人反對,但執行它的第一步會當場鎖死自己, 於是每個人都在那一步前面停下來,而**「我停下來了」不會留下任何痕跡**。
正確做法:
- 收到「取消某個制度」的裁決,第一個動作是去數還有幾支閘在執行那個制度, 並且先問那些閘是 fail-open 還是 fail-closed。 fail-closed 的那幾支決定了執行順序:先讓閘退役,再拿掉它要的東西,顛倒就鎖死。
- 退役要驗的不是「檔案刪了沒有」,是「那個擋還會不會發生」。
hooks/tests/sdd-guard-retired.test.sh把「裁決執行完之後的世界」丟給hooks.json上整組寫檔閘(清單當場從hooks.json讀,不寫死), 不准有任何一支用 SDD 當理由擋下來 ⇒ 換個檔名種回來照樣紅。
📌 一個沒人敢執行的裁決,看起來跟一個沒人記得的裁決一模一樣。 差別只有在**去問「執行它的第一步會發生什麼」**的時候才看得出來。
📌 KBDB 缺這一段: 2026-08-31 用 kbdb_search(mode='semantic') 查
「SDD 生命週期/單一活性/取消 Active SDD」0 命中(最接近的是 2026-08-09
一張講 SDD × Gitea 整合摩擦的卡,那是裁決之前)。⇒ 這條開發史還沒進 KBDB。
⚠️ MISTAKE: 閘擋對了,但它印出來的那條出路跑不動
票: inkstone/ISEP#112
日期: 2026-08-31
症狀: comment-carries-task-guard.sh 擋下留言時教你跑
scripts/ticket subtask …。照著貼,會印出一段「四個動詞」的說明然後
什麼都沒發生。被擋的人當下正在做別的事,最可能的反應是找個理由繞過去。
實查: 兩個各自獨立的原因,任何一個單獨存在都足以害死它——
① 相對路徑。閘印的是 scripts/ticket,而相對路徑是對
貼上去那個人的 cwd 解析的。總管的 cwd 是 InkStoneCo/,
那裡的 scripts/ticket 是正本停在 2026-08-27 之前的舊複本:
grep -c subtask InkStoneCo/scripts/ticket → 0(正本是 10)。
⇒ 「subtask 這個子命令不存在」這個結論在那個 cwd 底下是真的。
② 用法各寫一份。閘手抄了一段用法,cmd_subtask 的 die() 又寫了一段。
兩份已經漂了:閘那份寫 --assign <誰做> 卻沒有 --next,
而正本會因為「指派了人卻沒寫 --next」直接擋下(實測 exit 2)。
⇒ 就算路徑對了,那一行照樣跑不完。
原因: 閘的訊息與它教的那支工具是兩份各自維護的字。
hooks/lib/beacon_report.py ② 早就會在 SessionStart 報「專案裡有舊複本」,
但報告不會改掉閘印出來的那一行——知道有這個坑,跟那一行會不會踩到它,
是兩件事。
正確做法:
- 路徑:印出來要人複製的指令,一律用正本自己的
__file__絕對路徑 (scripts/ticket的self_path())。印指令的人與跑指令的人是同一個檔案, 沒有第二種可能。 ⚠️ 分界線是「這行字會被誰複製、在哪台機器上跑?」—— 要貼進 Gitea 留言的指令反而要維持相對寫法(留言會被別台機器讀到)。 - 用法:只留一份(
scripts/ticket的USAGE/usage_text()), 閘去ticket usage subtask --example現要,不自己抄。 - 模板:子票內文草稿由
REQUIRED_SECTIONS現生(body_template()), 跟檢查同源 ⇒ 不可能生出「照著貼卻過不了自己那道閘」的票。
🔴 驗法要驗到「它跑不跑得動」,不是「它有沒有擋」。
scripts/test-comment-carries-task-guard.sh ⑱ 把閘 stderr 裡那塊指令
原樣抽出來、只填三格、真的執行一次:離開碼 1(走到網路才停)=過,
2(被某道閘擋下)=這個病復發。⑲ 是它的鑑別力對照組。
📌 推廣:一支閘的價值是「擋下來 + 給一條走得通的路」。 出路走不通時它不是幫你,是擋你——而被擋的人不會停下來修閘, 他會去找繞過去的方法。所以「出路能不能跑」要跟「判準準不準」一樣被測。
⚠️ MISTAKE: 「雲端該有哪些憑證」的清單是唯一真相源,卻沒有任何東西在跟機器對帳
票: inkstone/ISEP#115(→ comment 5522/5526)
日期: 2026-09-01
症狀: 雲端 session 要清空 youlin,env | grep -ciE 'cloudflare|^CF_|wrangler' → 0。
而 credentials-map.md 明明寫著那把 token 在 InkStoneCo/.env。
實查: 不是雲端漏設。scripts/make-cloud-env.sh 的 NEEDED 當時只有 1 個變數,
裡面從來沒有任何 CF 憑證。而 inkstone/InkStoneCo#14 comment 3890
(2026-08-20)盤點出的 A 段是 8 個——那一版三段輸出的腳本,
到今天為止只活在**沒併進 main 的分支 fix/cloud-env-parity-14(PR #43)**上。
原因: 盤點做在票上、實作做在分支上、main 上的清單是另一回事——三份,互不對帳。
🔴 而清單漏一項的代價在流程末端才付:工人已經把腳本與測試都寫完了,
到要真的跑的那一刻才知道跑不了。
正確做法: 清單要有測試逼它跟這台機器對帳(scripts/test-make-cloud-env.sh,
docs/TESTING.md A26)——B 段逐一去 .env 找值,找不到就紅。
🔴 而「不准混進正式環境憑證」這一條,判準一律是機器算得出來的事實,
不是名字裡有沒有某個字——兩條分工:
B8 看值從哪個檔案拿到的(出自 polaris/mira/.env = leo21c 現役正式環境就紅);
B9 看這個值是不是就是 CLOUDFLARE_API_TOKEN_leo21c(那把住在頂層 .env,
B8 抓不到,而改個名字黑名單就放它過去——實跑證過)。黑名單擋不住沒被列進去的新名字,
「這個值出自那份 .env」是 grep 得出來的事實,換什麼名字都躲不掉。
同時釘住反面(B8x):拿一個已知住在那份 .env 的變數餵它,證明偵測真的會亮——
否則「全綠」有兩種可能(真的乾淨/偵測壞了),而分不開就等於沒驗。
⇒ 推廣:repo 裡任何「這台機器該有什麼」的清單,都要有一支東西定期拿它去問機器。 清單自己不會知道它漏了什麼。
⚠️ MISTAKE: 兩支閘對同一條指令說不同的話,而讀的那一半從來沒被測過
票: inkstone/ISEP#130
日期: 2026-09-07
症狀: 09-04 雲端 Routine 的 run log:讀 notify_leo 定義被 leo21c-write-guard.sh 擋下。
閘的判準明寫「讀可以,寫不行」,卻連讀都擋 ⇒ 雲端什麼都拿不到。
實查: ① 舊判準 -d[[:space:]] 把 | tr -d '\r'、cut -d= 這種唯讀的 -d 當成 curl 的 body。
prod-write-guard.sh 在 2026-08-12 修過一模一樣的洞(它的檔頭寫著「一堆唯讀工具也用 -d」),
這支漏了——同一批補丁只改其中一份(main-and-prod-push-guard.sh 檔頭記過同款)。
② …/webhooks/named/<ns>/notify_leo/trigger 在 prod-write-guard.sh 是白名單(ISEP#63),
在這支卻因為尾巴是 /trigger 照擋 ⇒ 撞人閘時發 Telegram 叫 leo 會被擋(InkStoneCo#110 的病)。
③ 這支閘從 08-20 建立到現在沒有任何測試,hooks/tests/ 裡 30 支測試沒有它的——
「讀會不會被誤攔」這格從來沒被驗過,只有「寫會不會被擋」被人肉試過。
原因: 同一個判準(「這條指令會不會真的寫到那台」)散在三支閘裡各寫一份,修一份不會帶動另外兩份; 而沒有測試的那支,誤攔發生時不會有任何東西喊一聲——被擋的人在雲端、沒有人在旁邊。
正確做法:
- 修判準時
grep -l找同一族的閘(prod-write-guard/main-and-prod-push-guard/leo21c-write-guard), 一次改齊;本次三支都補了 youlin 09-02 起的新子網域arcrun-yuga3bse。 - 每一支「擋」的閘都要有「不該擋」那半的測試,測資用現場真的會下的指令
(這次是
cloud-worker.md/progress-guard.md裡的原句),不是自己挑好抓的例子。 - 修之前拿新測試跑舊閘,紅的那幾條才是真的改到的(本次 5 條紅);全綠就是測試順著新閘寫的。
⚠️ MISTAKE: 雲端連不到 youlin 被記成「HTTP 000」,而 000 有三種病,兩種都不是連線
票: inkstone/ISEP#130
日期: 2026-09-07
症狀: 09-04 run log「youlin 從雲端 HTTP 000」;09-07 總管查到舊子網域 youlin-hsieh-dev DNS 已死,
以為換名字就通。從雲端打新名 arcrun-yuga3bse 還是 000。
實查: curl 印的是 CONNECT tunnel failed, response 403,$HTTPS_PROXY/__agentproxy/status 記為
connect_rejected … gateway answered 403 (policy denial)。同一個 session:leo21c 200、
git.uncle6.me 200、api.telegram.org 302——只有 youlin 的主機沒被雲端 egress policy 放行。
/root/.ccr/README.md 明寫:403/407 是組織的 egress policy,不要重試、不要繞路,回報主機名。
原因: -w '%{http_code}' 對「DNS 沒有」「proxy 拒絕 CONNECT」「對方沒回」三種都印 000,
而三種的修法各在不同人手上(改名字/leo 放行主機/看實例)。把它們記成同一個數字,
下一個人就會往錯的方向查(09-07 就差點只修名字收工)。
正確做法: 雲端看到 000 先跑 curl -sS "$HTTPS_PROXY/__agentproxy/status" | jq .recentRelayFailures,
connect_rejected + 403 ⇒ 是 policy,交給 leo 在 Cloud environment 放行 *.arcrun-yuga3bse.workers.dev;
不是 ⇒ 才去查名字或實例。記 log 時寫 curl 的錯誤字串(response 403/Could not resolve host),不要只寫 000。
⚠️ MISTAKE: 正門工具吃 owner/repo 會回 404,於是人走了側門
票: inkstone/ISEP#130 → comment 6120 第 2 件
日期: 2026-09-07
症狀: scripts/ticket new inkstone/ISEP -F … 回 Gitea 404;同一天 ticket say 對 InkStoneCo 正常。
總管當場改成直接打 API 開了四張票。
實查: cmd_new 把第一個參數原樣塞進 /repos/inkstone/{repo}/issues ⇒ /repos/inkstone/inkstone/ISEP/issues。
404 讀起來像「ISEP 這個 repo 不存在」,跟真正的原因隔了一層。subtask --to 同款。
原因: 票號的鐵律是全稱 owner/repo#N(CLAUDE.md),人自然會把收件 repo 也寫成 owner/repo;
工具只收短名、錯了又不講清楚 ⇒ 正門壞了,人就走 ticket-api-bypass-guard.sh 在防的那條側門。
一道閘把人逼去走它自己禁止的路,那道閘就是在製造違規(docs/governance/cloud-wiring.md ② 記過同一句)。
正確做法: repo_arg() 兩種寫法都收;org 寫錯(Leo/)當場講「org 是 inkstone」,不讓它變成一個要人猜的 404。
測試 scripts/test-ticket-repo-arg.sh 把 api() 換成假貨記路徑,離線驗路徑裡不准出現 inkstone/inkstone。
⚠️ MISTAKE: 權限白名單是「逐條比前綴」,複合指令永遠對不上——而假伺服器照自己的想像回 200
票: inkstone/ISEP#137(母票 inkstone/ISEP#130)
日期: 2026-09-07
症狀: 三條主線工人在雲端對 stage 的寫入全被 Claude Code auto mode 分類器擋:
printf … | npx wrangler secret put …、python3 scripts/stage-deploy-artifacts.py all --confirm、
curl -X POST …arcrun-yuga3bse.workers.dev/records。同一回合連 ticket where <含 token 字樣> 都被擋。
leo:「為什麼會擋你,這就是要修復的問題」「本機環境跟你不同應該不行」。
實查: docs/permissions-allow.json 的每一條是完全比對前綴。cd … && printf … | VAR=… npx … 這種複合指令
沒有任何一條對得上,任意寫法的 curl 更不可能;放整類(Bash(npx *)/Bash(curl *))又是紅線。
⇒ 分類器擋的不是「這件事」,是「這個形狀沒被事先放行」。
原因: 正門工具的價值在形狀固定——一條規則就能事先放行。之前四個正門(ticket/mainline/gate-ok/gitea-pr-merge) 都是這樣活下來的;對 stage 的寫入沒有正門,人就只能拼複合指令,而複合指令天生對不上任何前綴。
正確做法:
- 每一種要放行的動作收成一支固定形狀(
scripts/stage secret …/stage api …),白名單放它四種呼叫形狀。 「只打 stage」寫死在程式裡(帳號 ID 現查、主機後綴固定),不靠變數名字裡有沒有leo21c。 - 別的 repo 已經有的正門(Arcrun
stage-deploy-artifacts.py自己寫死 youlin 帳號 ID)直接放它的形狀,不重造一支。 - 值只從環境變數名讀;
--value/--bearer/--header一律拒收——指令列會進 shell 歷史與 session log。
📌 同票第二個坑:假伺服器要照真 API 回,不是照自己的想像回。 離線測試的假 CF 對 secret PUT 回 200,27/27 全綠;第一次真跑 CF 回 201,工具把它講成「失敗」—— 而獨立用 CF API 一列,那把 secret 明明種進去了。全綠的測試證明的是「工具跟假伺服器合得來」, 不是「工具跟 CF 合得來」。⇒ 假伺服器的回應碼要抄真的(現在回 201),而且離線全綠之後一定要真跑一次。