# 已知誤解 / 踩過的坑 > 這是 ISEP 自己的坑,不是 InkStoneCo 的(不轉抄,複製即 fork,fork 即漂移)。 > 撞到新坑就 append 一條;session 開場只推最近幾條標題(見 `hooks/session-start-recall.sh` push 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-`」兩行 **變成空白**。閘照擋 exit 2,所以測試若只看離開碼**完全看不出來**。 原因: 寫成 `cat >&2 <` 卻**沒有 `--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//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`。