fix(coolify,hetzner): 修正 Claude Code 桌面版 MCP env 傳遞失效 - #116
Conversation
* feat(codebase-sync): 強化深度審計 — 強制 Audit Report + 完整自動化偵測
- Step 0 新增 8 項強制自動化偵測(目錄/檔案存在性、scripts 差異、版本號現況)
- 引入強制 Audit Report 格式,每次執行都必須輸出,不得以「看起來沒問題」為由跳過
- 所有文件修改必須有對應 Audit Report finding,禁止無依據地動筆
- command 明確禁止跳步驟,強調「先審計後動筆」順序
- 新增未記載新功能偵測(實際存在但文件未提及的目錄/plugin)
* feat(codebase-sync): 加入 git 近期變動分析(0-J ~ 0-N)
- 新增 0-J:近期 30 筆 git commit 摘要
- 新增 0-K:近期 commit 涉及的檔案變動統計
- 新增 0-L:近期新增的檔案(可能尚未記載於文件)
- 新增 0-M:近期刪除的檔案(文件可能仍有引用)
- 新增 0-N:近期 feat/fix/refactor commits 過濾(最可能影響文件)
- Audit Report 加入 Section 8 git 近期變動分析
- 修正常見錯誤措辭:區分「用 git 找訊號」vs「把 commit message 複製進文件」
* fix(codebase-sync): 修正 code review 發現的 5 個問題
- fix(0-K): grep pattern 從 '^(A|D|R|M)\s' 改為 '^[ADMR][0-9]*\t',正確捕捉 git rename(R100 格式)
- fix(0-L/0-M): awk '{print $2}' 改為 cut -f2-,避免含空格的檔案路徑被截斷
- fix(Step 5-A): 移除不可執行的 'ls <每個新加的目錄路徑>' 佔位符,改為可操作的 find 說明
- fix(5-C): cat file | jq . 改為 jq . file(避免多餘 cat,更安全)
- fix(補充區塊): 移除與 Step 0-D 重複的 scripts 比對(regex 也不一致),只保留 .env.example env var 比對(唯一不在 Step 0 的檢查)
- feat(Audit Report): 新增 Section 9 記錄環境變數比對結果(補充區塊原本有比對但無處寫入)
* fix(codebase-sync): 0-G 補上 README.md 的檔案引用存在性檢查
原本 0-G 只檢查 README 中的目錄引用(/結尾),
不檢查 .ts/.json/.md 等檔案路徑,已刪除的檔案引用無法被偵測。
新增對應的檔案檢查區塊,與 0-F(CLAUDE.md)的邏輯對齊。
* fix(codebase-sync): 修正 grep pattern 跨平台相容性與點前綴路徑漏抓
- fix(0-K): \t 在 ERE 為實作相依,改用 POSIX [[:blank:]] 確保 macOS/GNU grep 皆正確匹配 tab
- fix(0-G): 目錄與檔案 regex 從 [a-z] 開頭改為 \.?[a-z0-9_],覆蓋 .github/、.env、.env.example 等點前綴路徑
裸 filename(如 marketplace.json)在 CLAUDE.md 散文中出現時, [ -f marketplace.json ] 會報 MISSING,但實際檔案在 .claude-plugin/marketplace.json。 修法:regex 強制要求至少含一個 /(<dir>/<file>.<ext> 格式), 純 filename 不含路徑的不會被提取,避免 Audit Report 產生誤導性 MISSING 條目。 同樣修正 0-G 的 README.md 檔案檢查,並補上 0-F 缺少的 \.? 點前綴支援。
… 傳遞失效 Claude Code Desktop app 從 launchd 啟動(launchd → Claude.app → disclaimer → claude → MCP server),整條行程鏈不經過任何 shell,因此 ~/.zshenv 從未被讀取。 再加上三個已知的 Desktop-only upstream bug: - anthropics/claude-code#40372:Desktop app 不展開 .mcp.json env 的 ${VAR} - anthropics/claude-code#42203 / #43210:Desktop app 忽略 settings.json 的 env 區塊 - #42203 留言:launchctl setenv 對子行程無效(會被覆寫) 結果是 ${COOLIFY_ACCESS_TOKEN} / ${HETZNER_API_TOKEN} 拿不到值,所有工具呼叫 一律 401。從終端機啟動 claude 則正常(父行程是 zsh,env 已繼承)。 改以 `/bin/zsh -lc "exec npx ..."` 啟動:env 解析發生在**子行程內部**, 不依賴 Claude Code 的展開器與 env 傳遞,因此不受上述三個 bug 影響。 同時移除 env 區塊,避免憑證來源一分為二。 why -lc 而非 -c:-c 只讀 .zshenv(拿得到 token 但 PATH 沒有 npx); -lc 為 login shell,會讀 .zshenv + .zprofile,兩者兼得。已驗證 login shell 在 exec 前不會輸出任何 stdout,不會污染 stdio JSON-RPC。 why 不含 langfuse:langfuse-mcp 的 published bin 使用 `#!/usr/bin/env bun` shebang,而 .bun/bin 僅存在於 .zshrc(互動式,-lc 不讀),套用後反而會讓原本 可用的 CLI 情境失效。該套件應改為 node shebang,另案處理。 驗證:以 env -i(僅 HOME + 最小 PATH,模擬 Desktop sandbox)spawn 本檔設定, coolify 與 hetzner 皆成功完成 MCP initialize handshake 並回傳 serverInfo。
承 fde9a60。code review 指出該 commit 引入安全迴歸: 原本 .mcp.json 的 env 區塊只把 2 個變數交給 MCP server(最小權限)。改成 `/bin/zsh -lc "exec npx ..."` 之後,子行程繼承整個 login shell 環境。以 node 探針實測,`npx -y @jurislm/coolify-mcp@latest`(runtime 才從 npm 抓下來的套件) 可以讀到 NPM_TOKEN、GITHUB_PERSONAL_ACCESS_TOKEN、HETZNER_API_TOKEN、 JURISLM_DRONE_RELEASE_PLEASE_TOKEN——遠比修正前寬鬆。 改為 `exec env -i <白名單> npx ...`:login shell 仍負責解析 ~/.zshenv,但外層 以 env -i 清空環境,只把該 plugin 實際需要的變數傳進去。保留 MCP SDK 本來就會 繼承的 HOME/LOGNAME/PATH/SHELL/TERM/USER(npx 需要 PATH 與 HOME)。 README 補上三件先前沒寫的前提: - 變數必須放 ~/.zshenv,不能放 ~/.zshrc(-lc 為 login shell,不讀 .zshrc) - 需要 macOS + /bin/zsh - env -i 的隔離範圍 驗證(env -i 模擬 Desktop app 實況,僅 MCP SDK 會傳的 6 個變數): - coolify list_applications → 正常回傳應用清單 - hetzner_list_ssh_keys → 正常回傳金鑰 - node 探針:NPM_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN / HETZNER_API_TOKEN 對 coolify server 皆已隔離,僅 COOLIFY_ACCESS_TOKEN / COOLIFY_BASE_URL 可見
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough本次變更新增 Changes審查自動化設定
MCP 環境變數隔離
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
此 PR 針對 Claude Code **桌面版(macOS / launchd 啟動鏈)**無法繼承自訂環境變數,導致 Coolify / Hetzner MCP server 取不到 token(Coolify 啟動即退出、Hetzner 呼叫時 401)的問題,改以 zsh -lc 主動載入 ~/.zshenv,並用 env -i 白名單隔離環境,避免多餘敏感變數外洩到 MCP 子行程。
Changes:
- 將
plugins/coolify、plugins/hetzner的.mcp.json改為透過/bin/zsh -lc啟動,並在內層用exec env -i <白名單>執行npx -y @jurislm/*-mcp@latest - 在 Coolify / Hetzner README 補充桌面版環境變數傳遞限制、
~/.zshenv放置要求與env -i隔離說明 - 新增
.coderabbit.yaml設定審查語言與忽略 release-please 產生的 release PR
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/hetzner/README.md | 補充桌面版 env 繼承限制與 zsh -lc/env -i 的使用前提說明 |
| plugins/hetzner/.mcp.json | 改用 zsh -lc 啟動並以 env -i 白名單方式傳遞必要變數 |
| plugins/coolify/README.md | 補充桌面版 env 繼承限制與 zsh -lc/env -i 的使用前提說明 |
| plugins/coolify/.mcp.json | 改用 zsh -lc 啟動並以 env -i 白名單方式傳遞必要變數 |
| .coderabbit.yaml | 設定 CodeRabbit 使用繁體中文語氣並忽略 release-please release PR |
1. `command` 由 `/bin/zsh` 改為 `zsh`(依 PATH 解析)
硬編絕對路徑把 plugin 綁死在 macOS——多數 Linux 發行版的 zsh 在
/usr/bin/zsh。實測桌面 App 的最小 PATH(/usr/bin:/bin:/usr/sbin:/sbin)
下 `zsh` 仍解析得到 /bin/zsh,改用 PATH 查找零成本且可攜。
已重新以該最小環境驗證兩支 server 皆正常。
2. README 修正 login shell 讀取的檔案清單
原文寫「只讀 ~/.zshenv + ~/.zprofile」不精確。實測(各檔植入 stderr 標記):
zsh -lc → .zshenv, .zprofile, .zlogin
zsh -c → .zshenv
兩者皆不讀 .zshrc。README 補上 .zlogin。
3. README 補上 stdout 污染警告
MCP 走 stdio JSON-RPC,上述任一啟動檔只要有 echo/printf,輸出就會排在第一個
JSON 訊息前面導致 handshake 失敗。實測:.zprofile 放一行 `echo GARBAGE`,
stdout 首行即為 GARBAGE。已在 README 指示改用 `print -u2`。
驗證:以 env -i(僅 MCP SDK 傳遞的 6 個變數 + 最小 PATH)spawn 新設定,
coolify list_applications 與 hetzner_list_ssh_keys 皆正常,且 stdout 首行
為合法 JSON(無前置污染)。
兩個 finding 都取證確認後修正。 1. fine-grained PAT 漏了 Administration: Read(Codex 於 jurislm-tools #251 指出) scripts/release-pr-auto-merge.mjs 會呼叫 GET /repos/{owner}/{repo}/branches/{branch}/protection 驗證分支保護設定。 GitHub 官方 fine-grained PAT 權限表明列該端點需要 Administration: Read。 照原本的清單(只有 Contents/Pull requests/Issues)鑄出來的 token,每個符合 資格的 release PR 都會在合併前 403 失敗。 這一項只適用有 release-pr-auto-merge pipeline 的 repo(lexvision、rimivo、 jurislm-tools、entire),因此以條件句寫入,不是無條件要求。 2. 「唯一可靠的症狀是版號不再前進」講太絕對(CodeRabbit 於 memory-dessert #116 指出) 我原本的意思是「不要靠 stage 的綠紅去推斷缺哪個權限」,但寫成了「其他症狀都 不可靠」,那是錯的:pipeline log 裡 release-please 的實際輸出與 GitHub API 回應都會顯示權限問題,403 回應甚至會帶 X-Accepted-GitHub-Permissions header 直接指出缺哪一項。改為指向這些真正的診斷來源,並提醒一併確認 Drone Web UI 上 secret 是否確實已設定。 五個 repo 的註解仍逐字相同(sha256 指紋 5e2f76a4ce3d),repo-standards 的 secret 表 與 SKILL.md 同步更新——那是權威來源,漏掉的話日後新建的 repo 會再踩一次同一個坑。
…共用範圍的描述
* refactor(ci): 將 RELEASE_PLEASE_TOKEN 統一為 GITHUB_API_TOKEN,並讓 repo-standards 反映其共用性質
JurisLM 各 repo 統一為單一 GitHub token 與單一名稱(見 jurislm/entire JUR-126、JUR-128)。
本 repo 是這批裡最要緊的一個:repo-standards plugin 定義了新 repo 的 CI 樣板與
建置 checklist。不改它,日後每個新建的 repo 都會沿用舊名字,等於改到一半又長回來。
## 改名範圍
.drone.yml、CLAUDE.md、scripts/{drone-ci-policy.test,validate-drone-config,
release-eligibility,release-eligibility.test,release-pr-auto-merge}.mjs、
openspec/specs/docs-and-standards/repo-standards-detail.md,以及 repo-standards
plugin 的 SKILL.md 與兩份 reference。openspec/changes/archive/ 是歷史記錄,不動。
## 不只改名:把描述改對
改了名字沒跟著改語意,正是第一批被外部審查抓到 3 個 Major 的根因。這次一併處理:
- validate-drone-config.mjs 與 drone-ci-policy.test.mjs 的錯誤訊息由「the release
token」「Drone release-token secret indirection」改為指名這是共用的 GitHub API
token。這顆 token 早就不只服務 release-please。
- openspec/specs/ci-platform/spec.md 兩處同樣的敘述一併更新。
## repo-standards 的三處實質補強
1. secret 表:原文只寫「release-please 寫 GitHub release PR / tag」。實際上
release-pr-auto-merge 與 deploy(讀 live main commit)也用它。表格改為列出全部
三種用途,並明寫它是跨各 repo 共用的同一份憑證,輪替時必須同步更新每一個 repo
的同名 secret——漏掉的 repo 是在那一邊靜默失敗,從操作當下的 repo 完全看不到。
同時補上 fine-grained PAT 需要 Issues: Read and write。release-please 以
autorelease: pending/tagged label 追蹤狀態,label 端點在 fine-grained 權限下
歸 Issues 而非 Pull requests。這條最常被漏掉,而漏掉的症狀是版號靜默不動。
取證:GitHub 官方 fine-grained PAT 權限表;且 memory-dessert #114、lawyer #237、
wedding #28、lexvision #224 這些已合併的 release PR 全都帶 autorelease: tagged。
2. new-repo-checklist 第 28 項:明寫新 repo 要用「現有的那一份共用憑證」,不要另鑄
一把。另鑄會讓日後輪替漏掉這個 repo。
3. SKILL.md 的規則行補上共用性質與 Issues 權限。
## 驗證
npm run validate:drone(validate + release + release-pr-auto-merge 三個 pipeline)、
npm run validate(160 tests 全過、plugin repository validation、version sync 1.41.1、
markdownlint)全數通過。
Drone secret GITHUB_API_TOKEN 已先建立;RELEASE_PLEASE_TOKEN 保留到本 PR 合併
且發版流程實測通過之後才刪除。
* docs(ci): 用實查結果校正 GITHUB_API_TOKEN 的 scope 敘述
我先前照抄 repo-standards 寫的「classic PAT scopes repo + workflow」,實查之後發現
三個數字互不相同:
- **repo-standards 原本寫的**:repo + workflow
- **實際需要的**:只有 repo。它已涵蓋建 PR、建 tag/release、貼 label、合併 PR。
workflow 只有在該 repo 有 .github/workflows/ 且 release-please 會改動它時才需要
——2026-08-22 逐一實查 entire / lawyer / memory-dessert / wedding / lexvision /
rimivo / jurislm-tools 七個 repo,全都沒有該目錄,且 release-please-config.json
的 extra-files 也沒有任何一項指向 workflow 檔。
- **實際持有的**:21 個 scope,含 admin:org、admin:enterprise、admin:org_hook、
delete_repo、write:packages 等。透過 GitHub API 回應的 X-OAuth-Scopes header 讀回。
也就是說這顆共用憑證是明顯的過度授權:一個只需要 repo 的用途,配了一把能刪 repo、
能改組織設定的鑰匙。文件現在把「最小需求」與「現況」分開寫,並註明下次輪替應收斂。
沒有在這次一併收斂 scope,因為那是會影響七個 repo 發版的動作,且該 token 同時被
GitHub MCP 與 gh api 使用,收斂前要先確認那些用途的需求——另行處理。
* docs(ci): 依外部審查補上 Administration: Read 並修正權限診斷指引
兩個 finding 都取證確認後修正。
1. fine-grained PAT 漏了 Administration: Read(Codex 於 jurislm-tools #251 指出)
scripts/release-pr-auto-merge.mjs 會呼叫
GET /repos/{owner}/{repo}/branches/{branch}/protection 驗證分支保護設定。
GitHub 官方 fine-grained PAT 權限表明列該端點需要 Administration: Read。
照原本的清單(只有 Contents/Pull requests/Issues)鑄出來的 token,每個符合
資格的 release PR 都會在合併前 403 失敗。
這一項只適用有 release-pr-auto-merge pipeline 的 repo(lexvision、rimivo、
jurislm-tools、entire),因此以條件句寫入,不是無條件要求。
2. 「唯一可靠的症狀是版號不再前進」講太絕對(CodeRabbit 於 memory-dessert #116 指出)
我原本的意思是「不要靠 stage 的綠紅去推斷缺哪個權限」,但寫成了「其他症狀都
不可靠」,那是錯的:pipeline log 裡 release-please 的實際輸出與 GitHub API
回應都會顯示權限問題,403 回應甚至會帶 X-Accepted-GitHub-Permissions header
直接指出缺哪一項。改為指向這些真正的診斷來源,並提醒一併確認 Drone Web UI 上
secret 是否確實已設定。
五個 repo 的註解仍逐字相同(sha256 指紋 5e2f76a4ce3d),repo-standards 的 secret 表
與 SKILL.md 同步更新——那是權威來源,漏掉的話日後新建的 repo 會再踩一次同一個坑。
問題
在 Claude Code 桌面版中,
/coolify完全沒有工具、/hetzner的工具一律回 401。從終端機啟動claude則一切正常。根因
桌面 App 從 launchd 啟動,整條行程鏈不經過任何 shell:
官方文件(Environment configuration → Local sessions)明載:
也就是自訂變數(
COOLIFY_ACCESS_TOKEN等)設計上就不繼承,~/.zshenv從未被讀取。再者,MCP SDK 的
DEFAULT_INHERITED_ENV_VARS只有HOME / LOGNAME / PATH / SHELL / TERM / USER——即使 token 在claude行程的 env 裡,子行程一樣拿不到。.mcp.json的env區塊是唯一通道,而其${VAR}需由桌面 App 從自己的 env 展開,那裡沒有這些變數。結果
${COOLIFY_ACCESS_TOKEN}/${HETZNER_API_TOKEN}展開成空字串,兩支 server 以不同方式失敗:coolify-mcpCOOLIFY_URL must be a valid URL)hetzner-mcp修法
~/.zshenv,不依賴 Claude Code 的展開器與 env 傳遞env -i只把該 plugin 需要的變數交給 MCP server為什麼
-lc而非-c:-c只讀.zshenv(拿得到 token 但 PATH 沒有 npx);-lc為 login shell,會讀.zshenv+.zprofile,兩者兼得。已驗證 login shell 在exec前不會輸出任何 stdout,不會污染 stdio JSON-RPC。Code review 追加修正(1af9e57)
單純用 login shell 會讓
npx -y ...@latest(runtime 才從 npm 抓下來的套件)繼承整個 login shell 環境。node 探針實測,coolify server 可讀到NPM_TOKEN、GITHUB_PERSONAL_ACCESS_TOKEN、HETZNER_API_TOKEN、JURISLM_DRONE_RELEASE_PLEASE_TOKEN——遠比原本env區塊的兩個變數寬鬆。加上
env -i白名單後,實測這些 token 皆已隔離。README 補上三項先前沒寫的前提:變數必須放
~/.zshenv而非~/.zshrc、需要 macOS +/bin/zsh、env -i的隔離範圍。為什麼不含 langfuse
langfuse-mcp的 published bin 使用#!/usr/bin/env bunshebang,而.bun/bin僅存在於.zshrc(-lc不讀),套用後反而會讓原本可用的 CLI 情境失效。已在 jurislm/langfuse-mcp#22 修正為 node runtime,待其發版後再比照套用。驗證
先在本機 plugin cache 套用、重啟 Claude Code 桌面版實測(非模擬):
/coolify45 個工具出現在 ToolSearch(修正前為 0)coolify list_applications→ 正常回傳 19 個 apphetzner_list_ssh_keys→ 正常(證明 token 到位)hetzner_list_servers→ 仍失敗,但屬另一個獨立 bug(Hetzner 移除datacenter欄位,見 fix(servers): 改用 location 取代已被 Hetzner 移除的 datacenter 欄位 hetzner-mcp#52)另以
env -i(僅 MCP SDK 會傳的 6 個變數,等同桌面 App 實況)spawn 本檔設定,兩支 server 均完成 handshake 並成功呼叫工具。Test plan
env -i模擬桌面 App 環境 spawn 兩支 server,handshake + 工具呼叫成功/coolify恢復Summary by CodeRabbit
文件更新
~/.zshenv。zsh的使用要求,並提醒避免多餘環境變數外洩。Chores