昨天接了一支別人的 MCP server,問題是「怎麼接」。今天往前一步:裝之前,你怎麼知道它會動你什麼?
現在的 AI 工具很少只是一支程式。它的 install 會偵測你裝了哪些 coding agent,然後替每一家寫 MCP 設定、注入 rules、放 skill、掛 hook —— 一個指令,散到 repo 和家目錄幾十個地方。而你看得到的只有三樣:README、--dry-run、裝完之後的 git status。前兩樣是工具自己說的,第三樣只看得到 repo 裡沒被 ignore 的那一塊。
今天拿 code-review-graph(以下簡稱 crg)當受試者 —— 一個把 codebase 掃成圖、讓 agent 少讀檔的工具,三萬多星,MIT 授權,怎麼裝已經很多人寫過。這篇不教怎麼裝它,是拿它示範:一個認真、誠實的工具裝下去之後,靠那三樣你能知道多少。 答案是一半不到。
Day 4 用過 checksum 前後比對,但那次只比 ~/.claude 一個目錄。安裝程式寫的地方多得多,所以這次網要撒大,而且先在副本上裝,不碰你真的 repo 和家目錄:
# 在要裝的 repo 根目錄執行
LAB=$(mktemp -d)
git clone -q --no-hardlinks . "$LAB/repo" # 1. 在副本上裝,不碰你的 repo
mkdir -p "$LAB/home"/{.codex,.copilot,.gemini/antigravity,.config/opencode}
touch "$LAB/home/.codex/config.toml" # 2. 假 HOME:只放你機器上真的有的工具目錄
snap() { (cd "$LAB" && find repo home repo/.git/hooks -path repo/.git -prune -o -type f -print0 \
| sort -z | xargs -0 shasum) > "$LAB/$1.txt"; } # 3. 快照:repo 含被 ignore 的、.git/hooks、假 HOME
CRG=$(uvx --from code-review-graph==2.3.8 which code-review-graph) # 先用真 HOME 把工具抓好
cd "$LAB/repo"
snap 0-before
HOME="$LAB/home" "$CRG" install --dry-run > "$LAB/dry-run.txt"
HOME="$LAB/home" "$CRG" install -y > "$LAB/install.txt"
snap 1-installed
git status --short > "$LAB/git-status.txt"
HOME="$LAB/home" "$CRG" uninstall -y > "$LAB/uninstall.txt"
snap 2-uninstalled
diff "$LAB/0-before.txt" "$LAB/1-installed.txt" | grep -c '^>' # 4. 裝了動了幾個
diff "$LAB/0-before.txt" "$LAB/2-uninstalled.txt" # 5. 移除後還差什麼
四個設計各有原因:
.claude/settings.json)不會跟著 clone 過來,要看它怎麼跟既有設定合併,就自己 cp 進去。HOME 指到一個空目錄,裡面只建你機器上真的有的工具目錄。安裝程式靠這些目錄判斷「你有裝 Codex」,所以它會照真實情況寫,只是寫進假的家。crg 是 Python,Path.home() 讀的就是 $HOME;我翻過它的原始碼,家目錄以外的路徑(/Applications 之類)只讀不寫。換成別的工具要自己確認這一點 —— 會寫 /usr/local、或不看 $HOME 的工具,假 HOME 攔不到,那種就直接開 VM。.git/hooks 和被 ignore 的檔:為什麼,下一節就看到。我在 apple/container 上跑,工具先抓好之後,整段 3 秒。
--dry-run 說 |
真的跑之後 | |
|---|---|---|
| MCP 設定 | 8 個寫入目標 | 8 個 ✅ |
| 指令注入 | 9 個檔 | 9 個 ✅ |
.gitignore |
會加一行 | 加了 ✅ |
| skills | 沒提 | 3 個平台 × 4 支 = 12 個 SKILL.md |
| hooks | 沒提 | 6 處(含 ~/.codex/hooks.json) |
| git hook | 沒提 | .git/hooks/pre-commit |
| OpenCode 外掛 | 沒提 | ~/.config/opencode/plugins/crg-plugin.ts |
| 備份檔 | 沒提 | .gemini/settings.json.bak |
--dry-run 漏掉的,除了一個備份檔,全是會改變 agent 行為、或會自己執行的那一類 —— skill 是 agent 會讀進去照做的指示,hook 是每次編輯後自動跑的指令,pre-commit 是每次 commit 前跑的腳本,OpenCode 外掛是一支會被載入執行的 TypeScript。它預告了設定檔,沒預告行為層的東西。加起來 39 個檔 —— 37 個新增、2 個改寫:
repo 內 33 個新檔(含 .git/hooks/pre-commit) + .gitignore 改 1 行
家目錄 4 個新檔 + ~/.codex/config.toml 加 6 行
git status 看不到的,大多是會自己跑的那一半裝完最常見的檢查是 git status。它列了 15 行:
M .gitignore
?? .codebuddy/
?? .cursorrules
?? .gemini/
?? .github/instructions/
?? .kiro/
?? .mcp.json
?? .qoder/
?? .windsurfrules
?? AGENTS.md
?? CLAUDE.md
?? CODEBUDDY.md
?? GEMINI.md
?? QODER.md
?? opencode.jsonc
看起來很完整。對照快照,它沒列出來的是這些:
| 位置 | 內容 | 為什麼看不到 |
|---|---|---|
.claude/settings.json、.claude/skills/ 4 支 |
Claude Code 的 hook 與 skill | .claude/ 在這個 repo 的 .gitignore 裡 |
.vscode/mcp.json |
VS Code Copilot 的 MCP 設定 | .vscode/ 同上 |
.git/hooks/pre-commit |
每次 commit 前會跑的腳本 | .git/ 從來不在 git status 裡 |
| 家目錄 5 個檔 | 全域 MCP 設定、全域 hook、OpenCode 外掛 | 不在 repo 裡 |
git status 看得到的是 rules 檔和 MCP 設定;看不到的是 Claude Code 的 hook 和 skill、pre-commit、全域設定 —— 會自己跑起來的,大多在看不到的那一邊。 這不是 crg 故意藏,是 .claude/ 本來就常被 ignore(Day 11 我自己的 repo 也是)。但結果一樣:只看 git status 的人,會以為它「就多了幾個 md 檔」。
~/.codex/config.toml 裡有一行寫死的 cwd:
[mcp_servers.code-review-graph]
command = "uvx"
args = ["code-review-graph", "serve"]
cwd = "/Volumes/.../Github/container" # ← 我當時所在的那個 repo
type = "stdio"
我在 A 專案裡裝了它,全域的 Codex 設定就永遠指向 A 專案。之後在 B 專案開 Codex,它問到的圖還是 A 的。
~/.codex/hooks.json 是全域的,matcher 是 Write|Edit|Bash:
"PostToolUse": [{ "matcher": "Write|Edit|Bash",
"hooks": [{ "command": "... code-review-graph update --skip-flows || true" }] }]
一次針對單一 repo 的 install,換來一個在你每一個專案、每一次編輯之後都會跑的 hook。
uninstall -y 移除 25 個路徑、編輯 14 個共用檔,全域搜尋 code-review-graph 字樣零命中。以一個裝了 39 個地方的工具來說,這算誠實。
但比對快照,它沒有回到裝之前的狀態:
~/.codex/hooks.json 留下 { } ← 裝之前不存在
~/.copilot/mcp-config.json 留下 {"mcpServers":{}}
~/.gemini/antigravity/mcp_config.json 留下 {"mcpServers":{}}
~/.codex/config.toml 多一個空行
repo 內 .claude/settings.json .vscode/mcp.json .mcp.json opencode.jsonc
.codebuddy/ .gemini/ .qoder/ 仍在
它清掉了自己寫的內容,但留下了自己建的容器。 我逐一驗過,每一個都是合法的空設定;但其中 .claude/settings.json 和 .vscode/mcp.json,git status 一樣看不到 —— 「乾淨移除」和「回到原狀」之間的差,只有快照看得見。
上面是空目錄。真實的機器上,.claude/settings.json、.mcp.json 多半本來就有你自己的東西 —— 第一次裝看它新增什麼,第二次要看它敢碰你什麼。
我在副本和假 HOME 裡先放好十個「自己的」設定檔,再重跑一次:.mcp.json 裡有一個 my-db server,.claude/settings.json 裡有 allow npm test、deny git push 和一個自己的 hook,CLAUDE.md 有兩條規則,家目錄的 Codex、Copilot 設定也各有東西。
install 很客氣。 十個檔全部是合併,沒有一個被覆蓋:你的 server、hook、權限規則都還在,它的東西加在旁邊;CLAUDE.md 在文末追加一段。要掛 hook 的 JSON,還先存了一份 .bak。
uninstall 大多也收得乾淨 —— CLAUDE.md、.gitignore 逐字還原,MCP 設定內容還原。只有一個檔例外,.claude/settings.json 的結尾變成這樣:
"PreToolUse": [ … 你自己的 hook … ],
}
它把自己的 hook 拿掉之後,留下一個多餘的逗號。這在嚴格的 JSON 裡是語法錯誤,而 Claude Code 讀這個檔用的就是嚴格 JSON。同一份內容,我對照跑了兩次:
| uninstall 留下的版本 | 只拿掉那個逗號 | |
|---|---|---|
| 你自己的 hook | 沒跑 | 有跑 |
deny: Bash(git push:*) |
失效,git push --dry-run 直接執行 |
擋下來 |
| Claude Code 有沒有提示 | claude -p 沒有;互動模式啟動時跳 Settings Error |
— |
互動模式下,Claude Code 一啟動就跳出 Settings Error,說「有錯的檔整份略過」,讓你選修、退出,或不帶這些設定繼續。但 claude -p、排程、CI 這些不互動的跑法,一聲不吭。
裝了再移除,你設定的「禁止 push」就悄悄沒了,至少在沒人看著的那些跑法裡。檔案還在,內容看起來也都對,只是整份不再生效;它又在 .claude/ 裡,git status 看不到。原因在它的原始碼:uninstall 改完會自己驗證一次,但驗證用的是會自動忽略多餘逗號的 JSONC 規則 —— 它驗過了,Claude Code 讀不了。2.3.9 也一樣重現,我回報給作者了(#1068)。
這種事空目錄測不出來。所以快照要多一個前提:副本裡要放一份你自己的設定。
量完之後,真的要裝的時候我會這樣做:
install --platform claude-code。9/13 我在 VM 裡用它裝了三次,家目錄 23 個設定檔前後零差異 —— 上面那些全域寫入,全是「偵測到你有 Codex、Copilot」才觸發的。多平台的工具,裝之前先找有沒有類似的旗標。python3 -m json.tool .claude/settings.json 一行就夠 —— 互動模式會跳 Settings Error,但 claude -p 和排程不會。還有一件快照看不到的事:crg 自己宣告 8 個相依,裝完落地 74 個套件。其中 opentelemetry 是 MCP SDK 的傳遞相依;uncalled-for 聽起來可疑,查了是個正經的 async DI 套件。兩個都不是問題,但我是查了才知道不是問題。 快照管的是它寫了哪些檔,管不到它帶進來的程式碼 —— 那是另一層。同樣管不到的還有它跑了什麼:連了哪裡、起了哪些程序;shasum 只比內容,連檔案權限被改都看不出來。快照回答的是「它寫了什麼」,不是「它做了什麼」。
知道它動了什麼,只回答了「裝了會怎樣」。另一半是「裝了值不值得」,這裡只講結論,實測細節在參考資料。
README 主打 ~82x median per-question token reduction(range 38x–528x,實驗當時的數字;9/13 重查已改成 ~65x)。它的分母是 naive full-corpus tokenization,也就是把整個 codebase 塞進去 —— 作者自己在旁邊註明:
upper bound no real agent actually pays
沒有 agent 真的那樣讀檔。Day 2 我引過 graphify 的「查一次 29k、整包塞進去 403k」,用的也是同一個分母,那個數字一樣要打這個折。
它最核心的承諾是「改這一行會炸到哪」。我拿 apple/container 三個真實 commit(改 3、4、20 個檔)驗了兩件事:
impact,三次都回 0 個受影響,還附一句 this 0 is a real absence。但同一張圖用 MCP 的 callers_of 問,中、大兩個 commit 分別回 3 個、4 個呼叫者,跟 git grep 一致 —— 邊在圖裡,impact 沒走到。CLAUDE.md 的「先問圖、再對原始碼」流程。所以就這三個 commit 看,我的答案是:大改動的 review 值得,日常不值得;而那套流程,不裝它也能寫進自己的 CLAUDE.md。
裝一個 AI 工具,你直接看得到的三樣東西,今天各漏了一塊:README 的倍數,分母不見了;--dry-run,漏了會改變行為、會自己執行的那一類;git status,看不到 .claude/、.git/hooks 和家目錄。後兩個洞疊在同一個地方 —— 會自己跑起來的東西。
所以方法只有一個:別問它動了什麼,自己拍快照。在副本上、用假 HOME、dry-run/install/uninstall 各一次,副本裡放一份你自己的設定。crg 那次,工具抓好之後整段 3 秒;方法本身換哪個工具都能用。
crg 不是反例。它 MIT、有 dry-run、有 uninstall,install 會合併、不覆蓋你的設定。正因為它已經是好的那一種,只靠它自己說的還是看不到一半 —— 連它的 uninstall 都會在你的設定檔留下一個讓規則失效的逗號。其他工具就更不用說了。
這一篇留下的心法:
工具說它要動什麼(dry-run)、說它清了什麼(uninstall),都要拿快照對過。
git status看不到的地方,正好是會自己跑起來的地方。
明天:一條我以為寫在 CLAUDE.md 裡三個月的規則,去查 git 才發現那三個月它連寫都沒寫 —— 所以這次把它寫成 hook,擋在寫入之前。
tirth8205/code-review-graph:github.com/tirth8205/code-review-graph(第一次查證 2026-07-27:26,608 stars、README 主打 ~82x;2026-09-13 重查:31.4k stars、README 已改為 ~65x、range 36x–376x;建立於 2026-02-26,MIT)。baseline 的註記與限制出自該 repo 的 READMEcode-review-graph 2.3.8(uvx)、apple/container @ d6de5694,腳本即本文那段,原樣跑過;HOME 指到只建了 .codex/、.gemini/antigravity/、.copilot/、.config/opencode/ 的暫存目錄。9/10 在主機真 HOME 裝過一次(當時做了 checksum 快照、實驗後 uninstall 並逐檔比對還原),家目錄的寫入與殘留跟假 HOME 這次一致settings.json 各用 claude -p --allowedTools Bash(Claude Code 2.1.282)驗 hook 與 deny;主機家目錄前後零差異;同一組對照在一台全域設定沒有任何權限規則的乾淨 VM(Claude Code 2.1.283)重跑,結果相同;互動模式另外開一次壞檔,啟動即跳 Settings Error(2.1.283)--platform claude-code 與六次 review:2026-09-13,VirtualBuddy VM(macOS 26.6.2、Claude Code 2.1.270、uv 0.12.13);三個 worktree 各 install -y --platform claude-code + build;claude -p --output-format json --max-turns 30;真值用主機的 git show 與 git grep